Skip to content

Commit 4539d1f

Browse files
committed
net/http: add ServeContent
Fixes #2039 R=r, rsc, n13m3y3r, r, rogpeppe CC=golang-dev https://golang.org/cl/5643067
1 parent 59dc215 commit 4539d1f

File tree

2 files changed

+178
-94
lines changed

2 files changed

+178
-94
lines changed

src/pkg/net/http/fs.go

Lines changed: 121 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
"strconv"
1818
"strings"
1919
"time"
20-
"unicode/utf8"
2120
)
2221

2322
// A Dir implements http.FileSystem using the native file
@@ -58,32 +57,6 @@ type File interface {
5857
Seek(offset int64, whence int) (int64, error)
5958
}
6059

61-
// Heuristic: b is text if it is valid UTF-8 and doesn't
62-
// contain any unprintable ASCII or Unicode characters.
63-
func isText(b []byte) bool {
64-
for len(b) > 0 && utf8.FullRune(b) {
65-
rune, size := utf8.DecodeRune(b)
66-
if size == 1 && rune == utf8.RuneError {
67-
// decoding error
68-
return false
69-
}
70-
if 0x7F <= rune && rune <= 0x9F {
71-
return false
72-
}
73-
if rune < ' ' {
74-
switch rune {
75-
case '\n', '\r', '\t':
76-
// okay
77-
default:
78-
// binary garbage
79-
return false
80-
}
81-
}
82-
b = b[size:]
83-
}
84-
return true
85-
}
86-
8760
func dirList(w ResponseWriter, f File) {
8861
w.Header().Set("Content-Type", "text/html; charset=utf-8")
8962
fmt.Fprintf(w, "<pre>\n")
@@ -104,6 +77,123 @@ func dirList(w ResponseWriter, f File) {
10477
fmt.Fprintf(w, "</pre>\n")
10578
}
10679

80+
// ServeContent replies to the request using the content in the
81+
// provided ReadSeeker. The main benefit of ServeContent over io.Copy
82+
// is that it handles Range requests properly, sets the MIME type, and
83+
// handles If-Modified-Since requests.
84+
//
85+
// If the response's Content-Type header is not set, ServeContent
86+
// first tries to deduce the type from name's file extension and,
87+
// if that fails, falls back to reading the first block of the content
88+
// and passing it to DetectContentType.
89+
// The name is otherwise unused; in particular it can be empty and is
90+
// never sent in the response.
91+
//
92+
// If modtime is not the zero time, ServeContent includes it in a
93+
// Last-Modified header in the response. If the request includes an
94+
// If-Modified-Since header, ServeContent uses modtime to decide
95+
// whether the content needs to be sent at all.
96+
//
97+
// The content's Seek method must work: ServeContent uses
98+
// a seek to the end of the content to determine its size.
99+
//
100+
// Note that *os.File implements the io.ReadSeeker interface.
101+
func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker) {
102+
size, err := content.Seek(0, os.SEEK_END)
103+
if err != nil {
104+
Error(w, "seeker can't seek", StatusInternalServerError)
105+
return
106+
}
107+
_, err = content.Seek(0, os.SEEK_SET)
108+
if err != nil {
109+
Error(w, "seeker can't seek", StatusInternalServerError)
110+
return
111+
}
112+
serveContent(w, req, name, modtime, size, content)
113+
}
114+
115+
// if name is empty, filename is unknown. (used for mime type, before sniffing)
116+
// if modtime.IsZero(), modtime is unknown.
117+
// content must be seeked to the beginning of the file.
118+
func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time, size int64, content io.ReadSeeker) {
119+
if checkLastModified(w, r, modtime) {
120+
return
121+
}
122+
123+
code := StatusOK
124+
125+
// If Content-Type isn't set, use the file's extension to find it.
126+
if w.Header().Get("Content-Type") == "" {
127+
ctype := mime.TypeByExtension(filepath.Ext(name))
128+
if ctype == "" {
129+
// read a chunk to decide between utf-8 text and binary
130+
var buf [1024]byte
131+
n, _ := io.ReadFull(content, buf[:])
132+
b := buf[:n]
133+
ctype = DetectContentType(b)
134+
_, err := content.Seek(0, os.SEEK_SET) // rewind to output whole file
135+
if err != nil {
136+
Error(w, "seeker can't seek", StatusInternalServerError)
137+
return
138+
}
139+
}
140+
w.Header().Set("Content-Type", ctype)
141+
}
142+
143+
// handle Content-Range header.
144+
// TODO(adg): handle multiple ranges
145+
sendSize := size
146+
if size >= 0 {
147+
ranges, err := parseRange(r.Header.Get("Range"), size)
148+
if err == nil && len(ranges) > 1 {
149+
err = errors.New("multiple ranges not supported")
150+
}
151+
if err != nil {
152+
Error(w, err.Error(), StatusRequestedRangeNotSatisfiable)
153+
return
154+
}
155+
if len(ranges) == 1 {
156+
ra := ranges[0]
157+
if _, err := content.Seek(ra.start, os.SEEK_SET); err != nil {
158+
Error(w, err.Error(), StatusRequestedRangeNotSatisfiable)
159+
return
160+
}
161+
sendSize = ra.length
162+
code = StatusPartialContent
163+
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", ra.start, ra.start+ra.length-1, size))
164+
}
165+
166+
w.Header().Set("Accept-Ranges", "bytes")
167+
if w.Header().Get("Content-Encoding") == "" {
168+
w.Header().Set("Content-Length", strconv.FormatInt(sendSize, 10))
169+
}
170+
}
171+
172+
w.WriteHeader(code)
173+
174+
if r.Method != "HEAD" {
175+
if sendSize == -1 {
176+
io.Copy(w, content)
177+
} else {
178+
io.CopyN(w, content, sendSize)
179+
}
180+
}
181+
}
182+
183+
// modtime is the modification time of the resource to be served, or IsZero().
184+
// return value is whether this request is now complete.
185+
func checkLastModified(w ResponseWriter, r *Request, modtime time.Time) bool {
186+
if modtime.IsZero() {
187+
return false
188+
}
189+
if t, err := time.Parse(TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modtime.After(t) {
190+
w.WriteHeader(StatusNotModified)
191+
return true
192+
}
193+
w.Header().Set("Last-Modified", modtime.UTC().Format(TimeFormat))
194+
return false
195+
}
196+
107197
// name is '/'-separated, not filepath.Separator.
108198
func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirect bool) {
109199
const indexPage = "/index.html"
@@ -148,14 +238,11 @@ func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirec
148238
}
149239
}
150240

151-
if t, err := time.Parse(TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && !d.ModTime().After(t) {
152-
w.WriteHeader(StatusNotModified)
153-
return
154-
}
155-
w.Header().Set("Last-Modified", d.ModTime().UTC().Format(TimeFormat))
156-
157241
// use contents of index.html for directory, if present
158242
if d.IsDir() {
243+
if checkLastModified(w, r, d.ModTime()) {
244+
return
245+
}
159246
index := name + indexPage
160247
ff, err := fs.Open(index)
161248
if err == nil {
@@ -174,60 +261,7 @@ func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirec
174261
return
175262
}
176263

177-
// serve file
178-
size := d.Size()
179-
code := StatusOK
180-
181-
// If Content-Type isn't set, use the file's extension to find it.
182-
if w.Header().Get("Content-Type") == "" {
183-
ctype := mime.TypeByExtension(filepath.Ext(name))
184-
if ctype == "" {
185-
// read a chunk to decide between utf-8 text and binary
186-
var buf [1024]byte
187-
n, _ := io.ReadFull(f, buf[:])
188-
b := buf[:n]
189-
if isText(b) {
190-
ctype = "text/plain; charset=utf-8"
191-
} else {
192-
// generic binary
193-
ctype = "application/octet-stream"
194-
}
195-
f.Seek(0, os.SEEK_SET) // rewind to output whole file
196-
}
197-
w.Header().Set("Content-Type", ctype)
198-
}
199-
200-
// handle Content-Range header.
201-
// TODO(adg): handle multiple ranges
202-
ranges, err := parseRange(r.Header.Get("Range"), size)
203-
if err == nil && len(ranges) > 1 {
204-
err = errors.New("multiple ranges not supported")
205-
}
206-
if err != nil {
207-
Error(w, err.Error(), StatusRequestedRangeNotSatisfiable)
208-
return
209-
}
210-
if len(ranges) == 1 {
211-
ra := ranges[0]
212-
if _, err := f.Seek(ra.start, os.SEEK_SET); err != nil {
213-
Error(w, err.Error(), StatusRequestedRangeNotSatisfiable)
214-
return
215-
}
216-
size = ra.length
217-
code = StatusPartialContent
218-
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", ra.start, ra.start+ra.length-1, d.Size()))
219-
}
220-
221-
w.Header().Set("Accept-Ranges", "bytes")
222-
if w.Header().Get("Content-Encoding") == "" {
223-
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
224-
}
225-
226-
w.WriteHeader(code)
227-
228-
if r.Method != "HEAD" {
229-
io.CopyN(w, f, size)
230-
}
264+
serveContent(w, r, d.Name(), d.ModTime(), d.Size(), f)
231265
}
232266

233267
// localRedirect gives a Moved Permanently response.

src/pkg/net/http/fs_test.go

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package http_test
66

77
import (
88
"fmt"
9+
"io"
910
"io/ioutil"
1011
. "net/http"
1112
"net/http/httptest"
@@ -14,6 +15,7 @@ import (
1415
"path/filepath"
1516
"strings"
1617
"testing"
18+
"time"
1719
)
1820

1921
const (
@@ -56,18 +58,18 @@ func TestServeFile(t *testing.T) {
5658
req.Method = "GET"
5759

5860
// straight GET
59-
_, body := getBody(t, req)
61+
_, body := getBody(t, "straight get", req)
6062
if !equal(body, file) {
6163
t.Fatalf("body mismatch: got %q, want %q", body, file)
6264
}
6365

6466
// Range tests
65-
for _, rt := range ServeFileRangeTests {
67+
for i, rt := range ServeFileRangeTests {
6668
req.Header.Set("Range", "bytes="+rt.r)
6769
if rt.r == "" {
6870
req.Header["Range"] = nil
6971
}
70-
r, body := getBody(t, req)
72+
r, body := getBody(t, fmt.Sprintf("test %d", i), req)
7173
if r.StatusCode != rt.code {
7274
t.Errorf("range=%q: StatusCode=%d, want %d", rt.r, r.StatusCode, rt.code)
7375
}
@@ -298,25 +300,73 @@ func TestServeIndexHtml(t *testing.T) {
298300
if err != nil {
299301
t.Fatal(err)
300302
}
301-
defer res.Body.Close()
302303
b, err := ioutil.ReadAll(res.Body)
303304
if err != nil {
304305
t.Fatal("reading Body:", err)
305306
}
306307
if s := string(b); s != want {
307308
t.Errorf("for path %q got %q, want %q", path, s, want)
308309
}
310+
res.Body.Close()
311+
}
312+
}
313+
314+
func TestServeContent(t *testing.T) {
315+
type req struct {
316+
name string
317+
modtime time.Time
318+
content io.ReadSeeker
319+
}
320+
ch := make(chan req, 1)
321+
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
322+
p := <-ch
323+
ServeContent(w, r, p.name, p.modtime, p.content)
324+
}))
325+
defer ts.Close()
326+
327+
css, err := os.Open("testdata/style.css")
328+
if err != nil {
329+
t.Fatal(err)
330+
}
331+
defer css.Close()
332+
333+
ch <- req{"style.css", time.Time{}, css}
334+
res, err := Get(ts.URL)
335+
if err != nil {
336+
t.Fatal(err)
337+
}
338+
if g, e := res.Header.Get("Content-Type"), "text/css; charset=utf-8"; g != e {
339+
t.Errorf("style.css: content type = %q, want %q", g, e)
340+
}
341+
if g := res.Header.Get("Last-Modified"); g != "" {
342+
t.Errorf("want empty Last-Modified; got %q", g)
343+
}
344+
345+
fi, err := css.Stat()
346+
if err != nil {
347+
t.Fatal(err)
348+
}
349+
ch <- req{"style.html", fi.ModTime(), css}
350+
res, err = Get(ts.URL)
351+
if err != nil {
352+
t.Fatal(err)
353+
}
354+
if g, e := res.Header.Get("Content-Type"), "text/html; charset=utf-8"; g != e {
355+
t.Errorf("style.html: content type = %q, want %q", g, e)
356+
}
357+
if g := res.Header.Get("Last-Modified"); g == "" {
358+
t.Errorf("want non-empty last-modified")
309359
}
310360
}
311361

312-
func getBody(t *testing.T, req Request) (*Response, []byte) {
362+
func getBody(t *testing.T, testName string, req Request) (*Response, []byte) {
313363
r, err := DefaultClient.Do(&req)
314364
if err != nil {
315-
t.Fatal(req.URL.String(), "send:", err)
365+
t.Fatalf("%s: for URL %q, send error: %v", testName, req.URL.String(), err)
316366
}
317367
b, err := ioutil.ReadAll(r.Body)
318368
if err != nil {
319-
t.Fatal("reading Body:", err)
369+
t.Fatalf("%s: for URL %q, reading body: %v", testName, req.URL.String(), err)
320370
}
321371
return r, b
322372
}

0 commit comments

Comments
 (0)