├── example ├── files │ └── readme.txt └── main.go ├── README.md └── fs.go /example/files/readme.txt: -------------------------------------------------------------------------------- 1 | This is a test file. -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/Masterminds/go-fileserver" 8 | ) 9 | 10 | func main() { 11 | 12 | // Specity a NotFoundHandler to use when no file is found. 13 | fileserver.NotFoundHandler = func(w http.ResponseWriter, req *http.Request) { 14 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") 15 | fmt.Fprintln(w, "That page could not be found.") 16 | } 17 | 18 | // Serve a directory of files. 19 | dir := http.Dir("./files") 20 | http.ListenAndServe(":8080", fileserver.FileServer(dir)) 21 | } 22 | 23 | // func main() { 24 | // http.HandleFunc("/", readme) 25 | // http.ListenAndServe(":8080", nil) 26 | // } 27 | 28 | // func readme(res http.ResponseWriter, req *http.Request) { 29 | // fileserver.ServeFile(res, req, "./files/readme.txt") 30 | // } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fileserver 2 | 3 | This project is a file server written in [Go](http://golang.org). The difference 4 | from the [standard library file server](http://golang.org/pkg/net/http/#FileServer) 5 | is that you can specify custom `Error` and `NotFound` handlers to respond with 6 | rather than being stuck using the built-in ones that only respond with text. 7 | 8 | _Note, this project is a fork of the Go source for serving files. The difference 9 | is just what was needed to implement the custom handlers._ 10 | 11 | ## Go in Practice 12 | This was inspired by the development of the book 13 | [Go in Practice](http://manning.com/butcher/) when writing about file service 14 | and solving some common problems. 15 | 16 | ## License 17 | The license is the same as [Go itself](https://github.com/golang/go/blob/master/LICENSE). 18 | 19 | ## Todo 20 | - Move the tests over. -------------------------------------------------------------------------------- /fs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Copyright 2015 Matthew Farina. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | // HTTP file system request handler 7 | 8 | package fileserver 9 | 10 | import ( 11 | "errors" 12 | "fmt" 13 | "io" 14 | "mime" 15 | "mime/multipart" 16 | "net/http" 17 | "net/textproto" 18 | "net/url" 19 | "os" 20 | "path" 21 | "path/filepath" 22 | "strconv" 23 | "strings" 24 | "time" 25 | ) 26 | 27 | // ErrorHandler is the package wide callback function to handle file server 28 | // error responses. The default is http.Error. 29 | var ErrorHandler ErrorHandlerFunc 30 | 31 | // NotFoundHandler is the package wide callback function to handle 404 not 32 | // found responses. The default is http.NotFound. 33 | var NotFoundHandler NotFoundFunc 34 | 35 | // ErrorHandlerFunc is the type for the package variable ErrorHandler. 36 | type ErrorHandlerFunc func(http.ResponseWriter, *http.Request, string, int) 37 | 38 | // NotFoundFunc is the type for the package variable NotFoundHandler. 39 | type NotFoundFunc func(http.ResponseWriter, *http.Request) 40 | 41 | // Error is the function to call when there is an error. If an ErrorHandler has 42 | // been specified that will be used. Otherwise it falls back to http.Error. 43 | func Error(w http.ResponseWriter, req *http.Request, error string, code int) { 44 | if ErrorHandler == nil { 45 | http.Error(w, error, code) 46 | } else { 47 | ErrorHandler(w, req, error, code) 48 | } 49 | } 50 | 51 | // NotFound is the function to call when there is a 404 not found response. If 52 | // a NotFoundHandler has been specified that will be used. The default is 53 | // http.NotFound. 54 | func NotFound(w http.ResponseWriter, req *http.Request) { 55 | if NotFoundHandler == nil { 56 | http.NotFound(w, req) 57 | } else { 58 | NotFoundHandler(w, req) 59 | } 60 | } 61 | 62 | // From net/http/server.go 63 | var htmlReplacer = strings.NewReplacer( 64 | "&", "&", 65 | "<", "<", 66 | ">", ">", 67 | // """ is shorter than """. 68 | `"`, """, 69 | // "'" is shorter than "'" and apos was not in HTML until HTML5. 70 | "'", "'", 71 | ) 72 | 73 | // From net/http/sniff.go 74 | // The algorithm uses at most sniffLen bytes to make its decision. 75 | const sniffLen = 512 76 | 77 | func dirList(w http.ResponseWriter, f http.File) { 78 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 79 | fmt.Fprintf(w, "
\n")
80 | for {
81 | dirs, err := f.Readdir(100)
82 | if err != nil || len(dirs) == 0 {
83 | break
84 | }
85 | for _, d := range dirs {
86 | name := d.Name()
87 | if d.IsDir() {
88 | name += "/"
89 | }
90 | // name may contain '?' or '#', which must be escaped to remain
91 | // part of the URL path, and not indicate the start of a query
92 | // string or fragment.
93 | url := url.URL{Path: name}
94 | fmt.Fprintf(w, "%s\n", url.String(), htmlReplacer.Replace(name))
95 | }
96 | }
97 | fmt.Fprintf(w, "\n")
98 | }
99 |
100 | // ServeContent replies to the request using the content in the
101 | // provided ReadSeeker. The main benefit of ServeContent over io.Copy
102 | // is that it handles Range requests properly, sets the MIME type, and
103 | // handles If-Modified-Since requests.
104 | //
105 | // If the response's Content-Type header is not set, ServeContent
106 | // first tries to deduce the type from name's file extension and,
107 | // if that fails, falls back to reading the first block of the content
108 | // and passing it to DetectContentType.
109 | // The name is otherwise unused; in particular it can be empty and is
110 | // never sent in the response.
111 | //
112 | // If modtime is not the zero time, ServeContent includes it in a
113 | // Last-Modified header in the response. If the request includes an
114 | // If-Modified-Since header, ServeContent uses modtime to decide
115 | // whether the content needs to be sent at all.
116 | //
117 | // The content's Seek method must work: ServeContent uses
118 | // a seek to the end of the content to determine its size.
119 | //
120 | // If the caller has set w's ETag header, ServeContent uses it to
121 | // handle requests using If-Range and If-None-Match.
122 | //
123 | // Note that *os.File implements the io.ReadSeeker interface.
124 | func ServeContent(w http.ResponseWriter, req *http.Request, name string, modtime time.Time, content io.ReadSeeker) {
125 | sizeFunc := func() (int64, error) {
126 | size, err := content.Seek(0, os.SEEK_END)
127 | if err != nil {
128 | return 0, errSeeker
129 | }
130 | _, err = content.Seek(0, os.SEEK_SET)
131 | if err != nil {
132 | return 0, errSeeker
133 | }
134 | return size, nil
135 | }
136 | serveContent(w, req, name, modtime, sizeFunc, content)
137 | }
138 |
139 | // errSeeker is returned by ServeContent's sizeFunc when the content
140 | // doesn't seek properly. The underlying Seeker's error text isn't
141 | // included in the sizeFunc reply so it's not sent over HTTP to end
142 | // users.
143 | var errSeeker = errors.New("seeker can't seek")
144 |
145 | // if name is empty, filename is unknown. (used for mime type, before sniffing)
146 | // if modtime.IsZero(), modtime is unknown.
147 | // content must be seeked to the beginning of the file.
148 | // The sizeFunc is called at most once. Its error, if any, is sent in the HTTP response.
149 | func serveContent(w http.ResponseWriter, r *http.Request, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker) {
150 | if checkLastModified(w, r, modtime) {
151 | return
152 | }
153 | rangeReq, done := checkETag(w, r, modtime)
154 | if done {
155 | return
156 | }
157 |
158 | code := http.StatusOK
159 |
160 | // If Content-Type isn't set, use the file's extension to find it, but
161 | // if the Content-Type is unset explicitly, do not sniff the type.
162 | ctypes, haveType := w.Header()["Content-Type"]
163 | var ctype string
164 | if !haveType {
165 | ctype = mime.TypeByExtension(filepath.Ext(name))
166 | if ctype == "" {
167 | // read a chunk to decide between utf-8 text and binary
168 | var buf [sniffLen]byte
169 | n, _ := io.ReadFull(content, buf[:])
170 | ctype = http.DetectContentType(buf[:n])
171 | _, err := content.Seek(0, os.SEEK_SET) // rewind to output whole file
172 | if err != nil {
173 | Error(w, r, "seeker can't seek", http.StatusInternalServerError)
174 | return
175 | }
176 | }
177 | w.Header().Set("Content-Type", ctype)
178 | } else if len(ctypes) > 0 {
179 | ctype = ctypes[0]
180 | }
181 |
182 | size, err := sizeFunc()
183 | if err != nil {
184 | Error(w, r, err.Error(), http.StatusInternalServerError)
185 | return
186 | }
187 |
188 | // handle Content-Range header.
189 | sendSize := size
190 | var sendContent io.Reader = content
191 | if size >= 0 {
192 | ranges, err := parseRange(rangeReq, size)
193 | if err != nil {
194 | Error(w, r, err.Error(), http.StatusRequestedRangeNotSatisfiable)
195 | return
196 | }
197 | if sumRangesSize(ranges) > size {
198 | // The total number of bytes in all the ranges
199 | // is larger than the size of the file by
200 | // itself, so this is probably an attack, or a
201 | // dumb client. Ignore the range request.
202 | ranges = nil
203 | }
204 | switch {
205 | case len(ranges) == 1:
206 | // RFC 2616, Section 14.16:
207 | // "When an HTTP message includes the content of a single
208 | // range (for example, a response to a request for a
209 | // single range, or to a request for a set of ranges
210 | // that overlap without any holes), this content is
211 | // transmitted with a Content-Range header, and a
212 | // Content-Length header showing the number of bytes
213 | // actually transferred.
214 | // ...
215 | // A response to a request for a single range MUST NOT
216 | // be sent using the multipart/byteranges media type."
217 | ra := ranges[0]
218 | if _, err := content.Seek(ra.start, os.SEEK_SET); err != nil {
219 | Error(w, r, err.Error(), http.StatusRequestedRangeNotSatisfiable)
220 | return
221 | }
222 | sendSize = ra.length
223 | code = http.StatusPartialContent
224 | w.Header().Set("Content-Range", ra.contentRange(size))
225 | case len(ranges) > 1:
226 | sendSize = rangesMIMESize(ranges, ctype, size)
227 | code = http.StatusPartialContent
228 |
229 | pr, pw := io.Pipe()
230 | mw := multipart.NewWriter(pw)
231 | w.Header().Set("Content-Type", "multipart/byteranges; boundary="+mw.Boundary())
232 | sendContent = pr
233 | defer pr.Close() // cause writing goroutine to fail and exit if CopyN doesn't finish.
234 | go func() {
235 | for _, ra := range ranges {
236 | part, err := mw.CreatePart(ra.mimeHeader(ctype, size))
237 | if err != nil {
238 | pw.CloseWithError(err)
239 | return
240 | }
241 | if _, err := content.Seek(ra.start, os.SEEK_SET); err != nil {
242 | pw.CloseWithError(err)
243 | return
244 | }
245 | if _, err := io.CopyN(part, content, ra.length); err != nil {
246 | pw.CloseWithError(err)
247 | return
248 | }
249 | }
250 | mw.Close()
251 | pw.Close()
252 | }()
253 | }
254 |
255 | w.Header().Set("Accept-Ranges", "bytes")
256 | if w.Header().Get("Content-Encoding") == "" {
257 | w.Header().Set("Content-Length", strconv.FormatInt(sendSize, 10))
258 | }
259 | }
260 |
261 | w.WriteHeader(code)
262 |
263 | if r.Method != "HEAD" {
264 | io.CopyN(w, sendContent, sendSize)
265 | }
266 | }
267 |
268 | // modtime is the modification time of the resource to be served, or IsZero().
269 | // return value is whether this request is now complete.
270 | func checkLastModified(w http.ResponseWriter, r *http.Request, modtime time.Time) bool {
271 | if modtime.IsZero() {
272 | return false
273 | }
274 |
275 | // The Date-Modified header truncates sub-second precision, so
276 | // use mtime < t+1s instead of mtime <= t to check for unmodified.
277 | if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modtime.Before(t.Add(1*time.Second)) {
278 | h := w.Header()
279 | delete(h, "Content-Type")
280 | delete(h, "Content-Length")
281 | w.WriteHeader(http.StatusNotModified)
282 | return true
283 | }
284 | w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat))
285 | return false
286 | }
287 |
288 | // checkETag implements If-None-Match and If-Range checks.
289 | //
290 | // The ETag or modtime must have been previously set in the
291 | // ResponseWriter's headers. The modtime is only compared at second
292 | // granularity and may be the zero value to mean unknown.
293 | //
294 | // The return value is the effective request "Range" header to use and
295 | // whether this request is now considered done.
296 | func checkETag(w http.ResponseWriter, r *http.Request, modtime time.Time) (rangeReq string, done bool) {
297 | etag := w.Header().Get("Etag")
298 | rangeReq = r.Header.Get("Range")
299 |
300 | // Invalidate the range request if the entity doesn't match the one
301 | // the client was expecting.
302 | // "If-Range: version" means "ignore the Range: header unless version matches the
303 | // current file."
304 | // We only support ETag versions.
305 | // The caller must have set the ETag on the response already.
306 | if ir := r.Header.Get("If-Range"); ir != "" && ir != etag {
307 | // The If-Range value is typically the ETag value, but it may also be
308 | // the modtime date. See golang.org/issue/8367.
309 | timeMatches := false
310 | if !modtime.IsZero() {
311 | if t, err := http.ParseTime(ir); err == nil && t.Unix() == modtime.Unix() {
312 | timeMatches = true
313 | }
314 | }
315 | if !timeMatches {
316 | rangeReq = ""
317 | }
318 | }
319 |
320 | if inm := r.Header.Get("If-None-Match"); inm != "" {
321 | // Must know ETag.
322 | if etag == "" {
323 | return rangeReq, false
324 | }
325 |
326 | // TODO(bradfitz): non-GET/HEAD requests require more work:
327 | // sending a different status code on matches, and
328 | // also can't use weak cache validators (those with a "W/
329 | // prefix). But most users of ServeContent will be using
330 | // it on GET or HEAD, so only support those for now.
331 | if r.Method != "GET" && r.Method != "HEAD" {
332 | return rangeReq, false
333 | }
334 |
335 | // TODO(bradfitz): deal with comma-separated or multiple-valued
336 | // list of If-None-match values. For now just handle the common
337 | // case of a single item.
338 | if inm == etag || inm == "*" {
339 | h := w.Header()
340 | delete(h, "Content-Type")
341 | delete(h, "Content-Length")
342 | w.WriteHeader(http.StatusNotModified)
343 | return "", true
344 | }
345 | }
346 | return rangeReq, false
347 | }
348 |
349 | // name is '/'-separated, not filepath.Separator.
350 | func serveFile(w http.ResponseWriter, r *http.Request, fs http.FileSystem, name string, redirect bool) {
351 | const indexPage = "/index.html"
352 |
353 | // redirect .../index.html to .../
354 | // can't use Redirect() because that would make the path absolute,
355 | // which would be a problem running under StripPrefix
356 | if strings.HasSuffix(r.URL.Path, indexPage) {
357 | localRedirect(w, r, "./")
358 | return
359 | }
360 |
361 | f, err := fs.Open(name)
362 | if err != nil {
363 | // TODO expose actual error?
364 | NotFound(w, r)
365 | return
366 | }
367 | defer f.Close()
368 |
369 | d, err1 := f.Stat()
370 | if err1 != nil {
371 | // TODO expose actual error?
372 | NotFound(w, r)
373 | return
374 | }
375 |
376 | if redirect {
377 | // redirect to canonical path: / at end of directory url
378 | // r.URL.Path always begins with /
379 | url := r.URL.Path
380 | if d.IsDir() {
381 | if url[len(url)-1] != '/' {
382 | localRedirect(w, r, path.Base(url)+"/")
383 | return
384 | }
385 | } else {
386 | if url[len(url)-1] == '/' {
387 | localRedirect(w, r, "../"+path.Base(url))
388 | return
389 | }
390 | }
391 | }
392 |
393 | // use contents of index.html for directory, if present
394 | if d.IsDir() {
395 | index := strings.TrimSuffix(name, "/") + indexPage
396 | ff, err := fs.Open(index)
397 | if err == nil {
398 | defer ff.Close()
399 | dd, err := ff.Stat()
400 | if err == nil {
401 | name = index
402 | d = dd
403 | f = ff
404 | }
405 | }
406 | }
407 |
408 | // Still a directory? (we didn't find an index.html file)
409 | if d.IsDir() {
410 | if checkLastModified(w, r, d.ModTime()) {
411 | return
412 | }
413 | dirList(w, f)
414 | return
415 | }
416 |
417 | // serveContent will check modification time
418 | sizeFunc := func() (int64, error) { return d.Size(), nil }
419 | serveContent(w, r, d.Name(), d.ModTime(), sizeFunc, f)
420 | }
421 |
422 | // localRedirect gives a Moved Permanently response.
423 | // It does not convert relative paths to absolute paths like Redirect does.
424 | func localRedirect(w http.ResponseWriter, r *http.Request, newPath string) {
425 | if q := r.URL.RawQuery; q != "" {
426 | newPath += "?" + q
427 | }
428 | w.Header().Set("Location", newPath)
429 | w.WriteHeader(http.StatusMovedPermanently)
430 | }
431 |
432 | // ServeFile replies to the request with the contents of the named file or directory.
433 | func ServeFile(w http.ResponseWriter, r *http.Request, name string) {
434 | dir, file := filepath.Split(name)
435 | serveFile(w, r, http.Dir(dir), file, false)
436 | }
437 |
438 | type fileHandler struct {
439 | root http.FileSystem
440 | }
441 |
442 | // FileServer returns a handler that serves HTTP requests
443 | // with the contents of the file system rooted at root.
444 | //
445 | // To use the operating system's file system implementation,
446 | // use http.Dir:
447 | //
448 | // http.Handle("/", http.FileServer(http.Dir("/tmp")))
449 | func FileServer(root http.FileSystem) http.Handler {
450 | return &fileHandler{root}
451 | }
452 |
453 | func (f *fileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
454 | upath := r.URL.Path
455 | if !strings.HasPrefix(upath, "/") {
456 | upath = "/" + upath
457 | r.URL.Path = upath
458 | }
459 | serveFile(w, r, f.root, path.Clean(upath), true)
460 | }
461 |
462 | // httpRange specifies the byte range to be sent to the client.
463 | type httpRange struct {
464 | start, length int64
465 | }
466 |
467 | func (r httpRange) contentRange(size int64) string {
468 | return fmt.Sprintf("bytes %d-%d/%d", r.start, r.start+r.length-1, size)
469 | }
470 |
471 | func (r httpRange) mimeHeader(contentType string, size int64) textproto.MIMEHeader {
472 | return textproto.MIMEHeader{
473 | "Content-Range": {r.contentRange(size)},
474 | "Content-Type": {contentType},
475 | }
476 | }
477 |
478 | // parseRange parses a Range header string as per RFC 2616.
479 | func parseRange(s string, size int64) ([]httpRange, error) {
480 | if s == "" {
481 | return nil, nil // header not present
482 | }
483 | const b = "bytes="
484 | if !strings.HasPrefix(s, b) {
485 | return nil, errors.New("invalid range")
486 | }
487 | var ranges []httpRange
488 | for _, ra := range strings.Split(s[len(b):], ",") {
489 | ra = strings.TrimSpace(ra)
490 | if ra == "" {
491 | continue
492 | }
493 | i := strings.Index(ra, "-")
494 | if i < 0 {
495 | return nil, errors.New("invalid range")
496 | }
497 | start, end := strings.TrimSpace(ra[:i]), strings.TrimSpace(ra[i+1:])
498 | var r httpRange
499 | if start == "" {
500 | // If no start is specified, end specifies the
501 | // range start relative to the end of the file.
502 | i, err := strconv.ParseInt(end, 10, 64)
503 | if err != nil {
504 | return nil, errors.New("invalid range")
505 | }
506 | if i > size {
507 | i = size
508 | }
509 | r.start = size - i
510 | r.length = size - r.start
511 | } else {
512 | i, err := strconv.ParseInt(start, 10, 64)
513 | if err != nil || i >= size || i < 0 {
514 | return nil, errors.New("invalid range")
515 | }
516 | r.start = i
517 | if end == "" {
518 | // If no end is specified, range extends to end of the file.
519 | r.length = size - r.start
520 | } else {
521 | i, err := strconv.ParseInt(end, 10, 64)
522 | if err != nil || r.start > i {
523 | return nil, errors.New("invalid range")
524 | }
525 | if i >= size {
526 | i = size - 1
527 | }
528 | r.length = i - r.start + 1
529 | }
530 | }
531 | ranges = append(ranges, r)
532 | }
533 | return ranges, nil
534 | }
535 |
536 | // countingWriter counts how many bytes have been written to it.
537 | type countingWriter int64
538 |
539 | func (w *countingWriter) Write(p []byte) (n int, err error) {
540 | *w += countingWriter(len(p))
541 | return len(p), nil
542 | }
543 |
544 | // rangesMIMESize returns the number of bytes it takes to encode the
545 | // provided ranges as a multipart response.
546 | func rangesMIMESize(ranges []httpRange, contentType string, contentSize int64) (encSize int64) {
547 | var w countingWriter
548 | mw := multipart.NewWriter(&w)
549 | for _, ra := range ranges {
550 | mw.CreatePart(ra.mimeHeader(contentType, contentSize))
551 | encSize += ra.length
552 | }
553 | mw.Close()
554 | encSize += int64(w)
555 | return
556 | }
557 |
558 | func sumRangesSize(ranges []httpRange) (size int64) {
559 | for _, ra := range ranges {
560 | size += ra.length
561 | }
562 | return
563 | }
564 |
--------------------------------------------------------------------------------