├── 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 | --------------------------------------------------------------------------------