github.com/FenixAra/go@v0.0.0-20170127160404-96ea0918e670/src/net/http/fs.go (about) 1 // Copyright 2009 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // HTTP file system request handler 6 7 package http 8 9 import ( 10 "errors" 11 "fmt" 12 "io" 13 "mime" 14 "mime/multipart" 15 "net/textproto" 16 "net/url" 17 "os" 18 "path" 19 "path/filepath" 20 "sort" 21 "strconv" 22 "strings" 23 "time" 24 ) 25 26 // A Dir implements FileSystem using the native file system restricted to a 27 // specific directory tree. 28 // 29 // While the FileSystem.Open method takes '/'-separated paths, a Dir's string 30 // value is a filename on the native file system, not a URL, so it is separated 31 // by filepath.Separator, which isn't necessarily '/'. 32 // 33 // An empty Dir is treated as ".". 34 type Dir string 35 36 func (d Dir) Open(name string) (File, error) { 37 if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) || 38 strings.Contains(name, "\x00") { 39 return nil, errors.New("http: invalid character in file path") 40 } 41 dir := string(d) 42 if dir == "" { 43 dir = "." 44 } 45 f, err := os.Open(filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name)))) 46 if err != nil { 47 return nil, err 48 } 49 return f, nil 50 } 51 52 // A FileSystem implements access to a collection of named files. 53 // The elements in a file path are separated by slash ('/', U+002F) 54 // characters, regardless of host operating system convention. 55 type FileSystem interface { 56 Open(name string) (File, error) 57 } 58 59 // A File is returned by a FileSystem's Open method and can be 60 // served by the FileServer implementation. 61 // 62 // The methods should behave the same as those on an *os.File. 63 type File interface { 64 io.Closer 65 io.Reader 66 io.Seeker 67 Readdir(count int) ([]os.FileInfo, error) 68 Stat() (os.FileInfo, error) 69 } 70 71 func dirList(w ResponseWriter, f File) { 72 dirs, err := f.Readdir(-1) 73 if err != nil { 74 // TODO: log err.Error() to the Server.ErrorLog, once it's possible 75 // for a handler to get at its Server via the ResponseWriter. See 76 // Issue 12438. 77 Error(w, "Error reading directory", StatusInternalServerError) 78 return 79 } 80 sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() }) 81 82 w.Header().Set("Content-Type", "text/html; charset=utf-8") 83 fmt.Fprintf(w, "<pre>\n") 84 for _, d := range dirs { 85 name := d.Name() 86 if d.IsDir() { 87 name += "/" 88 } 89 // name may contain '?' or '#', which must be escaped to remain 90 // part of the URL path, and not indicate the start of a query 91 // string or fragment. 92 url := url.URL{Path: name} 93 fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", url.String(), htmlReplacer.Replace(name)) 94 } 95 fmt.Fprintf(w, "</pre>\n") 96 } 97 98 // ServeContent replies to the request using the content in the 99 // provided ReadSeeker. The main benefit of ServeContent over io.Copy 100 // is that it handles Range requests properly, sets the MIME type, and 101 // handles If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since, 102 // and If-Range requests. 103 // 104 // If the response's Content-Type header is not set, ServeContent 105 // first tries to deduce the type from name's file extension and, 106 // if that fails, falls back to reading the first block of the content 107 // and passing it to DetectContentType. 108 // The name is otherwise unused; in particular it can be empty and is 109 // never sent in the response. 110 // 111 // If modtime is not the zero time or Unix epoch, ServeContent 112 // includes it in a Last-Modified header in the response. If the 113 // request includes an If-Modified-Since header, ServeContent uses 114 // modtime to decide whether the content needs to be sent at all. 115 // 116 // The content's Seek method must work: ServeContent uses 117 // a seek to the end of the content to determine its size. 118 // 119 // If the caller has set w's ETag header formatted per RFC 7232, section 2.3, 120 // ServeContent uses it to handle requests using If-Match, If-None-Match, or If-Range. 121 // 122 // Note that *os.File implements the io.ReadSeeker interface. 123 func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker) { 124 sizeFunc := func() (int64, error) { 125 size, err := content.Seek(0, io.SeekEnd) 126 if err != nil { 127 return 0, errSeeker 128 } 129 _, err = content.Seek(0, io.SeekStart) 130 if err != nil { 131 return 0, errSeeker 132 } 133 return size, nil 134 } 135 serveContent(w, req, name, modtime, sizeFunc, content) 136 } 137 138 // errSeeker is returned by ServeContent's sizeFunc when the content 139 // doesn't seek properly. The underlying Seeker's error text isn't 140 // included in the sizeFunc reply so it's not sent over HTTP to end 141 // users. 142 var errSeeker = errors.New("seeker can't seek") 143 144 // errNoOverlap is returned by serveContent's parseRange if first-byte-pos of 145 // all of the byte-range-spec values is greater than the content size. 146 var errNoOverlap = errors.New("invalid range: failed to overlap") 147 148 // if name is empty, filename is unknown. (used for mime type, before sniffing) 149 // if modtime.IsZero(), modtime is unknown. 150 // content must be seeked to the beginning of the file. 151 // The sizeFunc is called at most once. Its error, if any, is sent in the HTTP response. 152 func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker) { 153 setLastModified(w, modtime) 154 done, rangeReq := checkPreconditions(w, r, modtime) 155 if done { 156 return 157 } 158 159 code := StatusOK 160 161 // If Content-Type isn't set, use the file's extension to find it, but 162 // if the Content-Type is unset explicitly, do not sniff the type. 163 ctypes, haveType := w.Header()["Content-Type"] 164 var ctype string 165 if !haveType { 166 ctype = mime.TypeByExtension(filepath.Ext(name)) 167 if ctype == "" { 168 // read a chunk to decide between utf-8 text and binary 169 var buf [sniffLen]byte 170 n, _ := io.ReadFull(content, buf[:]) 171 ctype = DetectContentType(buf[:n]) 172 _, err := content.Seek(0, io.SeekStart) // rewind to output whole file 173 if err != nil { 174 Error(w, "seeker can't seek", StatusInternalServerError) 175 return 176 } 177 } 178 w.Header().Set("Content-Type", ctype) 179 } else if len(ctypes) > 0 { 180 ctype = ctypes[0] 181 } 182 183 size, err := sizeFunc() 184 if err != nil { 185 Error(w, err.Error(), StatusInternalServerError) 186 return 187 } 188 189 // handle Content-Range header. 190 sendSize := size 191 var sendContent io.Reader = content 192 if size >= 0 { 193 ranges, err := parseRange(rangeReq, size) 194 if err != nil { 195 if err == errNoOverlap { 196 w.Header().Set("Content-Range", fmt.Sprintf("bytes */%d", size)) 197 } 198 Error(w, err.Error(), StatusRequestedRangeNotSatisfiable) 199 return 200 } 201 if sumRangesSize(ranges) > size { 202 // The total number of bytes in all the ranges 203 // is larger than the size of the file by 204 // itself, so this is probably an attack, or a 205 // dumb client. Ignore the range request. 206 ranges = nil 207 } 208 switch { 209 case len(ranges) == 1: 210 // RFC 2616, Section 14.16: 211 // "When an HTTP message includes the content of a single 212 // range (for example, a response to a request for a 213 // single range, or to a request for a set of ranges 214 // that overlap without any holes), this content is 215 // transmitted with a Content-Range header, and a 216 // Content-Length header showing the number of bytes 217 // actually transferred. 218 // ... 219 // A response to a request for a single range MUST NOT 220 // be sent using the multipart/byteranges media type." 221 ra := ranges[0] 222 if _, err := content.Seek(ra.start, io.SeekStart); err != nil { 223 Error(w, err.Error(), StatusRequestedRangeNotSatisfiable) 224 return 225 } 226 sendSize = ra.length 227 code = StatusPartialContent 228 w.Header().Set("Content-Range", ra.contentRange(size)) 229 case len(ranges) > 1: 230 sendSize = rangesMIMESize(ranges, ctype, size) 231 code = StatusPartialContent 232 233 pr, pw := io.Pipe() 234 mw := multipart.NewWriter(pw) 235 w.Header().Set("Content-Type", "multipart/byteranges; boundary="+mw.Boundary()) 236 sendContent = pr 237 defer pr.Close() // cause writing goroutine to fail and exit if CopyN doesn't finish. 238 go func() { 239 for _, ra := range ranges { 240 part, err := mw.CreatePart(ra.mimeHeader(ctype, size)) 241 if err != nil { 242 pw.CloseWithError(err) 243 return 244 } 245 if _, err := content.Seek(ra.start, io.SeekStart); err != nil { 246 pw.CloseWithError(err) 247 return 248 } 249 if _, err := io.CopyN(part, content, ra.length); err != nil { 250 pw.CloseWithError(err) 251 return 252 } 253 } 254 mw.Close() 255 pw.Close() 256 }() 257 } 258 259 w.Header().Set("Accept-Ranges", "bytes") 260 if w.Header().Get("Content-Encoding") == "" { 261 w.Header().Set("Content-Length", strconv.FormatInt(sendSize, 10)) 262 } 263 } 264 265 w.WriteHeader(code) 266 267 if r.Method != "HEAD" { 268 io.CopyN(w, sendContent, sendSize) 269 } 270 } 271 272 // scanETag determines if a syntactically valid ETag is present at s. If so, 273 // the ETag and remaining text after consuming ETag is returned. Otherwise, 274 // it returns "", "". 275 func scanETag(s string) (etag string, remain string) { 276 s = textproto.TrimString(s) 277 start := 0 278 if strings.HasPrefix(s, "W/") { 279 start = 2 280 } 281 if len(s[start:]) < 2 || s[start] != '"' { 282 return "", "" 283 } 284 // ETag is either W/"text" or "text". 285 // See RFC 7232 2.3. 286 for i := start + 1; i < len(s); i++ { 287 c := s[i] 288 switch { 289 // Character values allowed in ETags. 290 case c == 0x21 || c >= 0x23 && c <= 0x7E || c >= 0x80: 291 case c == '"': 292 return string(s[:i+1]), s[i+1:] 293 default: 294 break 295 } 296 } 297 return "", "" 298 } 299 300 // etagStrongMatch reports whether a and b match using strong ETag comparison. 301 // Assumes a and b are valid ETags. 302 func etagStrongMatch(a, b string) bool { 303 return a == b && a != "" && a[0] == '"' 304 } 305 306 // etagWeakMatch reports whether a and b match using weak ETag comparison. 307 // Assumes a and b are valid ETags. 308 func etagWeakMatch(a, b string) bool { 309 return strings.TrimPrefix(a, "W/") == strings.TrimPrefix(b, "W/") 310 } 311 312 // condResult is the result of an HTTP request precondition check. 313 // See https://tools.ietf.org/html/rfc7232 section 3. 314 type condResult int 315 316 const ( 317 condNone condResult = iota 318 condTrue 319 condFalse 320 ) 321 322 func checkIfMatch(w ResponseWriter, r *Request) condResult { 323 im := r.Header.Get("If-Match") 324 if im == "" { 325 return condNone 326 } 327 for { 328 im = textproto.TrimString(im) 329 if len(im) == 0 { 330 break 331 } 332 if im[0] == ',' { 333 im = im[1:] 334 continue 335 } 336 if im[0] == '*' { 337 return condTrue 338 } 339 etag, remain := scanETag(im) 340 if etag == "" { 341 break 342 } 343 if etagStrongMatch(etag, w.Header().get("Etag")) { 344 return condTrue 345 } 346 im = remain 347 } 348 349 return condFalse 350 } 351 352 func checkIfUnmodifiedSince(w ResponseWriter, r *Request, modtime time.Time) condResult { 353 ius := r.Header.Get("If-Unmodified-Since") 354 if ius == "" || isZeroTime(modtime) { 355 return condNone 356 } 357 if t, err := ParseTime(ius); err == nil { 358 // The Date-Modified header truncates sub-second precision, so 359 // use mtime < t+1s instead of mtime <= t to check for unmodified. 360 if modtime.Before(t.Add(1 * time.Second)) { 361 return condTrue 362 } 363 return condFalse 364 } 365 return condNone 366 } 367 368 func checkIfNoneMatch(w ResponseWriter, r *Request) condResult { 369 inm := r.Header.get("If-None-Match") 370 if inm == "" { 371 return condNone 372 } 373 buf := inm 374 for { 375 buf = textproto.TrimString(buf) 376 if len(buf) == 0 { 377 break 378 } 379 if buf[0] == ',' { 380 buf = buf[1:] 381 } 382 if buf[0] == '*' { 383 return condFalse 384 } 385 etag, remain := scanETag(buf) 386 if etag == "" { 387 break 388 } 389 if etagWeakMatch(etag, w.Header().get("Etag")) { 390 return condFalse 391 } 392 buf = remain 393 } 394 return condTrue 395 } 396 397 func checkIfModifiedSince(w ResponseWriter, r *Request, modtime time.Time) condResult { 398 if r.Method != "GET" && r.Method != "HEAD" { 399 return condNone 400 } 401 ims := r.Header.Get("If-Modified-Since") 402 if ims == "" || isZeroTime(modtime) { 403 return condNone 404 } 405 t, err := ParseTime(ims) 406 if err != nil { 407 return condNone 408 } 409 // The Date-Modified header truncates sub-second precision, so 410 // use mtime < t+1s instead of mtime <= t to check for unmodified. 411 if modtime.Before(t.Add(1 * time.Second)) { 412 return condFalse 413 } 414 return condTrue 415 } 416 417 func checkIfRange(w ResponseWriter, r *Request, modtime time.Time) condResult { 418 if r.Method != "GET" { 419 return condNone 420 } 421 ir := r.Header.get("If-Range") 422 if ir == "" { 423 return condNone 424 } 425 etag, _ := scanETag(ir) 426 if etag != "" { 427 if etagStrongMatch(etag, w.Header().Get("Etag")) { 428 return condTrue 429 } else { 430 return condFalse 431 } 432 } 433 // The If-Range value is typically the ETag value, but it may also be 434 // the modtime date. See golang.org/issue/8367. 435 if modtime.IsZero() { 436 return condFalse 437 } 438 t, err := ParseTime(ir) 439 if err != nil { 440 return condFalse 441 } 442 if t.Unix() == modtime.Unix() { 443 return condTrue 444 } 445 return condFalse 446 } 447 448 var unixEpochTime = time.Unix(0, 0) 449 450 // isZeroTime reports whether t is obviously unspecified (either zero or Unix()=0). 451 func isZeroTime(t time.Time) bool { 452 return t.IsZero() || t.Equal(unixEpochTime) 453 } 454 455 func setLastModified(w ResponseWriter, modtime time.Time) { 456 if !isZeroTime(modtime) { 457 w.Header().Set("Last-Modified", modtime.UTC().Format(TimeFormat)) 458 } 459 } 460 461 func writeNotModified(w ResponseWriter) { 462 // RFC 7232 section 4.1: 463 // a sender SHOULD NOT generate representation metadata other than the 464 // above listed fields unless said metadata exists for the purpose of 465 // guiding cache updates (e.g., Last-Modified might be useful if the 466 // response does not have an ETag field). 467 h := w.Header() 468 delete(h, "Content-Type") 469 delete(h, "Content-Length") 470 if h.Get("Etag") != "" { 471 delete(h, "Last-Modified") 472 } 473 w.WriteHeader(StatusNotModified) 474 } 475 476 // checkPreconditions evaluates request preconditions and reports whether a precondition 477 // resulted in sending StatusNotModified or StatusPreconditionFailed. 478 func checkPreconditions(w ResponseWriter, r *Request, modtime time.Time) (done bool, rangeHeader string) { 479 // This function carefully follows RFC 7232 section 6. 480 ch := checkIfMatch(w, r) 481 if ch == condNone { 482 ch = checkIfUnmodifiedSince(w, r, modtime) 483 } 484 if ch == condFalse { 485 w.WriteHeader(StatusPreconditionFailed) 486 return true, "" 487 } 488 switch checkIfNoneMatch(w, r) { 489 case condFalse: 490 if r.Method == "GET" || r.Method == "HEAD" { 491 writeNotModified(w) 492 return true, "" 493 } else { 494 w.WriteHeader(StatusPreconditionFailed) 495 return true, "" 496 } 497 case condNone: 498 if checkIfModifiedSince(w, r, modtime) == condFalse { 499 writeNotModified(w) 500 return true, "" 501 } 502 } 503 504 rangeHeader = r.Header.get("Range") 505 if rangeHeader != "" { 506 if checkIfRange(w, r, modtime) == condFalse { 507 rangeHeader = "" 508 } 509 } 510 return false, rangeHeader 511 } 512 513 // name is '/'-separated, not filepath.Separator. 514 func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirect bool) { 515 const indexPage = "/index.html" 516 517 // redirect .../index.html to .../ 518 // can't use Redirect() because that would make the path absolute, 519 // which would be a problem running under StripPrefix 520 if strings.HasSuffix(r.URL.Path, indexPage) { 521 localRedirect(w, r, "./") 522 return 523 } 524 525 f, err := fs.Open(name) 526 if err != nil { 527 msg, code := toHTTPError(err) 528 Error(w, msg, code) 529 return 530 } 531 defer f.Close() 532 533 d, err := f.Stat() 534 if err != nil { 535 msg, code := toHTTPError(err) 536 Error(w, msg, code) 537 return 538 } 539 540 if redirect { 541 // redirect to canonical path: / at end of directory url 542 // r.URL.Path always begins with / 543 url := r.URL.Path 544 if d.IsDir() { 545 if url[len(url)-1] != '/' { 546 localRedirect(w, r, path.Base(url)+"/") 547 return 548 } 549 } else { 550 if url[len(url)-1] == '/' { 551 localRedirect(w, r, "../"+path.Base(url)) 552 return 553 } 554 } 555 } 556 557 // redirect if the directory name doesn't end in a slash 558 if d.IsDir() { 559 url := r.URL.Path 560 if url[len(url)-1] != '/' { 561 localRedirect(w, r, path.Base(url)+"/") 562 return 563 } 564 } 565 566 // use contents of index.html for directory, if present 567 if d.IsDir() { 568 index := strings.TrimSuffix(name, "/") + indexPage 569 ff, err := fs.Open(index) 570 if err == nil { 571 defer ff.Close() 572 dd, err := ff.Stat() 573 if err == nil { 574 name = index 575 d = dd 576 f = ff 577 } 578 } 579 } 580 581 // Still a directory? (we didn't find an index.html file) 582 if d.IsDir() { 583 if checkIfModifiedSince(w, r, d.ModTime()) == condFalse { 584 writeNotModified(w) 585 return 586 } 587 w.Header().Set("Last-Modified", d.ModTime().UTC().Format(TimeFormat)) 588 dirList(w, f) 589 return 590 } 591 592 // serveContent will check modification time 593 sizeFunc := func() (int64, error) { return d.Size(), nil } 594 serveContent(w, r, d.Name(), d.ModTime(), sizeFunc, f) 595 } 596 597 // toHTTPError returns a non-specific HTTP error message and status code 598 // for a given non-nil error value. It's important that toHTTPError does not 599 // actually return err.Error(), since msg and httpStatus are returned to users, 600 // and historically Go's ServeContent always returned just "404 Not Found" for 601 // all errors. We don't want to start leaking information in error messages. 602 func toHTTPError(err error) (msg string, httpStatus int) { 603 if os.IsNotExist(err) { 604 return "404 page not found", StatusNotFound 605 } 606 if os.IsPermission(err) { 607 return "403 Forbidden", StatusForbidden 608 } 609 // Default: 610 return "500 Internal Server Error", StatusInternalServerError 611 } 612 613 // localRedirect gives a Moved Permanently response. 614 // It does not convert relative paths to absolute paths like Redirect does. 615 func localRedirect(w ResponseWriter, r *Request, newPath string) { 616 if q := r.URL.RawQuery; q != "" { 617 newPath += "?" + q 618 } 619 w.Header().Set("Location", newPath) 620 w.WriteHeader(StatusMovedPermanently) 621 } 622 623 // ServeFile replies to the request with the contents of the named 624 // file or directory. 625 // 626 // If the provided file or directory name is a relative path, it is 627 // interpreted relative to the current directory and may ascend to parent 628 // directories. If the provided name is constructed from user input, it 629 // should be sanitized before calling ServeFile. As a precaution, ServeFile 630 // will reject requests where r.URL.Path contains a ".." path element. 631 // 632 // As a special case, ServeFile redirects any request where r.URL.Path 633 // ends in "/index.html" to the same path, without the final 634 // "index.html". To avoid such redirects either modify the path or 635 // use ServeContent. 636 func ServeFile(w ResponseWriter, r *Request, name string) { 637 if containsDotDot(r.URL.Path) { 638 // Too many programs use r.URL.Path to construct the argument to 639 // serveFile. Reject the request under the assumption that happened 640 // here and ".." may not be wanted. 641 // Note that name might not contain "..", for example if code (still 642 // incorrectly) used filepath.Join(myDir, r.URL.Path). 643 Error(w, "invalid URL path", StatusBadRequest) 644 return 645 } 646 dir, file := filepath.Split(name) 647 serveFile(w, r, Dir(dir), file, false) 648 } 649 650 func containsDotDot(v string) bool { 651 if !strings.Contains(v, "..") { 652 return false 653 } 654 for _, ent := range strings.FieldsFunc(v, isSlashRune) { 655 if ent == ".." { 656 return true 657 } 658 } 659 return false 660 } 661 662 func isSlashRune(r rune) bool { return r == '/' || r == '\\' } 663 664 type fileHandler struct { 665 root FileSystem 666 } 667 668 // FileServer returns a handler that serves HTTP requests 669 // with the contents of the file system rooted at root. 670 // 671 // To use the operating system's file system implementation, 672 // use http.Dir: 673 // 674 // http.Handle("/", http.FileServer(http.Dir("/tmp"))) 675 // 676 // As a special case, the returned file server redirects any request 677 // ending in "/index.html" to the same path, without the final 678 // "index.html". 679 func FileServer(root FileSystem) Handler { 680 return &fileHandler{root} 681 } 682 683 func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) { 684 upath := r.URL.Path 685 if !strings.HasPrefix(upath, "/") { 686 upath = "/" + upath 687 r.URL.Path = upath 688 } 689 serveFile(w, r, f.root, path.Clean(upath), true) 690 } 691 692 // httpRange specifies the byte range to be sent to the client. 693 type httpRange struct { 694 start, length int64 695 } 696 697 func (r httpRange) contentRange(size int64) string { 698 return fmt.Sprintf("bytes %d-%d/%d", r.start, r.start+r.length-1, size) 699 } 700 701 func (r httpRange) mimeHeader(contentType string, size int64) textproto.MIMEHeader { 702 return textproto.MIMEHeader{ 703 "Content-Range": {r.contentRange(size)}, 704 "Content-Type": {contentType}, 705 } 706 } 707 708 // parseRange parses a Range header string as per RFC 2616. 709 // errNoOverlap is returned if none of the ranges overlap. 710 func parseRange(s string, size int64) ([]httpRange, error) { 711 if s == "" { 712 return nil, nil // header not present 713 } 714 const b = "bytes=" 715 if !strings.HasPrefix(s, b) { 716 return nil, errors.New("invalid range") 717 } 718 var ranges []httpRange 719 noOverlap := false 720 for _, ra := range strings.Split(s[len(b):], ",") { 721 ra = strings.TrimSpace(ra) 722 if ra == "" { 723 continue 724 } 725 i := strings.Index(ra, "-") 726 if i < 0 { 727 return nil, errors.New("invalid range") 728 } 729 start, end := strings.TrimSpace(ra[:i]), strings.TrimSpace(ra[i+1:]) 730 var r httpRange 731 if start == "" { 732 // If no start is specified, end specifies the 733 // range start relative to the end of the file. 734 i, err := strconv.ParseInt(end, 10, 64) 735 if err != nil { 736 return nil, errors.New("invalid range") 737 } 738 if i > size { 739 i = size 740 } 741 r.start = size - i 742 r.length = size - r.start 743 } else { 744 i, err := strconv.ParseInt(start, 10, 64) 745 if err != nil || i < 0 { 746 return nil, errors.New("invalid range") 747 } 748 if i >= size { 749 // If the range begins after the size of the content, 750 // then it does not overlap. 751 noOverlap = true 752 continue 753 } 754 r.start = i 755 if end == "" { 756 // If no end is specified, range extends to end of the file. 757 r.length = size - r.start 758 } else { 759 i, err := strconv.ParseInt(end, 10, 64) 760 if err != nil || r.start > i { 761 return nil, errors.New("invalid range") 762 } 763 if i >= size { 764 i = size - 1 765 } 766 r.length = i - r.start + 1 767 } 768 } 769 ranges = append(ranges, r) 770 } 771 if noOverlap && len(ranges) == 0 { 772 // The specified ranges did not overlap with the content. 773 return nil, errNoOverlap 774 } 775 return ranges, nil 776 } 777 778 // countingWriter counts how many bytes have been written to it. 779 type countingWriter int64 780 781 func (w *countingWriter) Write(p []byte) (n int, err error) { 782 *w += countingWriter(len(p)) 783 return len(p), nil 784 } 785 786 // rangesMIMESize returns the number of bytes it takes to encode the 787 // provided ranges as a multipart response. 788 func rangesMIMESize(ranges []httpRange, contentType string, contentSize int64) (encSize int64) { 789 var w countingWriter 790 mw := multipart.NewWriter(&w) 791 for _, ra := range ranges { 792 mw.CreatePart(ra.mimeHeader(contentType, contentSize)) 793 encSize += ra.length 794 } 795 mw.Close() 796 encSize += int64(w) 797 return 798 } 799 800 func sumRangesSize(ranges []httpRange) (size int64) { 801 for _, ra := range ranges { 802 size += ra.length 803 } 804 return 805 }