@@ -17,7 +17,6 @@ import (
17
17
"strconv"
18
18
"strings"
19
19
"time"
20
- "unicode/utf8"
21
20
)
22
21
23
22
// A Dir implements http.FileSystem using the native file
@@ -58,32 +57,6 @@ type File interface {
58
57
Seek (offset int64 , whence int ) (int64 , error )
59
58
}
60
59
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
-
87
60
func dirList (w ResponseWriter , f File ) {
88
61
w .Header ().Set ("Content-Type" , "text/html; charset=utf-8" )
89
62
fmt .Fprintf (w , "<pre>\n " )
@@ -104,6 +77,123 @@ func dirList(w ResponseWriter, f File) {
104
77
fmt .Fprintf (w , "</pre>\n " )
105
78
}
106
79
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
+
107
197
// name is '/'-separated, not filepath.Separator.
108
198
func serveFile (w ResponseWriter , r * Request , fs FileSystem , name string , redirect bool ) {
109
199
const indexPage = "/index.html"
@@ -148,14 +238,11 @@ func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirec
148
238
}
149
239
}
150
240
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
-
157
241
// use contents of index.html for directory, if present
158
242
if d .IsDir () {
243
+ if checkLastModified (w , r , d .ModTime ()) {
244
+ return
245
+ }
159
246
index := name + indexPage
160
247
ff , err := fs .Open (index )
161
248
if err == nil {
@@ -174,60 +261,7 @@ func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirec
174
261
return
175
262
}
176
263
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 )
231
265
}
232
266
233
267
// localRedirect gives a Moved Permanently response.
0 commit comments