// Copyright 2009 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.// HTTP file system request handlerpackage httpimport ()// A Dir implements [FileSystem] using the native file system restricted to a// specific directory tree.//// While the [FileSystem.Open] method takes '/'-separated paths, a Dir's string// value is a directory path on the native file system, not a URL, so it is separated// by [filepath.Separator], which isn't necessarily '/'.//// Note that Dir could expose sensitive files and directories. Dir will follow// symlinks pointing out of the directory tree, which can be especially dangerous// if serving from a directory in which users are able to create arbitrary symlinks.// Dir will also allow access to files and directories starting with a period,// which could expose sensitive directories like .git or sensitive files like// .htpasswd. To exclude files with a leading period, remove the files/directories// from the server or create a custom FileSystem implementation.//// An empty Dir is treated as ".".typeDirstring// mapOpenError maps the provided non-nil error from opening name// to a possibly better non-nil error. In particular, it turns OS-specific errors// about opening files in non-directories into fs.ErrNotExist. See Issues 18984 and 49552.func mapOpenError( error, string, rune, func(string) (fs.FileInfo, error)) error {iferrors.Is(, fs.ErrNotExist) || errors.Is(, fs.ErrPermission) {return } := strings.Split(, string())for := range {if [] == "" {continue } , := (strings.Join([:+1], string()))if != nil {return }if !.IsDir() {returnfs.ErrNotExist } }return}// Open implements [FileSystem] using [os.Open], opening files for reading rooted// and relative to the directory d.func ( Dir) ( string) (File, error) { := path.Clean("/" + )[1:]if == "" { = "." } , := filepath.Localize()if != nil {returnnil, errors.New("http: invalid or unsafe file path") } := string()if == "" { = "." } := filepath.Join(, ) , := os.Open()if != nil {returnnil, mapOpenError(, , filepath.Separator, os.Stat) }return , nil}// A FileSystem implements access to a collection of named files.// The elements in a file path are separated by slash ('/', U+002F)// characters, regardless of host operating system convention.// See the [FileServer] function to convert a FileSystem to a [Handler].//// This interface predates the [fs.FS] interface, which can be used instead:// the [FS] adapter function converts an fs.FS to a FileSystem.typeFileSysteminterface {Open(name string) (File, error)}// A File is returned by a [FileSystem]'s Open method and can be// served by the [FileServer] implementation.//// The methods should behave the same as those on an [*os.File].typeFileinterface {io.Closerio.Readerio.SeekerReaddir(count int) ([]fs.FileInfo, error)Stat() (fs.FileInfo, error)}type anyDirs interface { len() int name(i int) string isDir(i int) bool}type fileInfoDirs []fs.FileInfofunc ( fileInfoDirs) () int { returnlen() }func ( fileInfoDirs) ( int) bool { return [].IsDir() }func ( fileInfoDirs) ( int) string { return [].Name() }type dirEntryDirs []fs.DirEntryfunc ( dirEntryDirs) () int { returnlen() }func ( dirEntryDirs) ( int) bool { return [].IsDir() }func ( dirEntryDirs) ( int) string { return [].Name() }func dirList( ResponseWriter, *Request, File) {// Prefer to use ReadDir instead of Readdir, // because the former doesn't require calling // Stat on every entry of a directory on Unix.varanyDirsvarerrorif , := .(fs.ReadDirFile); {vardirEntryDirs , = .ReadDir(-1) = } else {varfileInfoDirs , = .Readdir(-1) = }if != nil {logf(, "http: error reading directory: %v", )Error(, "Error reading directory", StatusInternalServerError)return }sort.Slice(, func(, int) bool { return .name() < .name() }) .Header().Set("Content-Type", "text/html; charset=utf-8")fmt.Fprintf(, "<!doctype html>\n")fmt.Fprintf(, "<meta name=\"viewport\" content=\"width=device-width\">\n")fmt.Fprintf(, "<pre>\n")for , := 0, .len(); < ; ++ { := .name()if .isDir() { += "/" }// name may contain '?' or '#', which must be escaped to remain // part of the URL path, and not indicate the start of a query // string or fragment. := url.URL{Path: }fmt.Fprintf(, "<a href=\"%s\">%s</a>\n", .String(), htmlReplacer.Replace()) }fmt.Fprintf(, "</pre>\n")}// GODEBUG=httpservecontentkeepheaders=1 restores the pre-1.23 behavior of not deleting// Cache-Control, Content-Encoding, Etag, or Last-Modified headers on ServeContent errors.var httpservecontentkeepheaders = godebug.New("httpservecontentkeepheaders")// serveError serves an error from ServeFile, ServeFileFS, and ServeContent.// Because those can all be configured by the caller by setting headers like// Etag, Last-Modified, and Cache-Control to send on a successful response,// the error path needs to clear them, since they may not be meant for errors.func serveError( ResponseWriter, string, int) { := .Header() := falsefor , := range []string{"Cache-Control","Content-Encoding","Etag","Last-Modified", } {if !.has() {continue }ifhttpservecontentkeepheaders.Value() == "1" { = true } else { .Del() } }if {httpservecontentkeepheaders.IncNonDefault() }Error(, , )}// ServeContent replies to the request using the content in the// provided ReadSeeker. The main benefit of ServeContent over [io.Copy]// is that it handles Range requests properly, sets the MIME type, and// handles If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since,// and If-Range requests.//// If the response's Content-Type header is not set, ServeContent// first tries to deduce the type from name's file extension and,// if that fails, falls back to reading the first block of the content// and passing it to [DetectContentType].// The name is otherwise unused; in particular it can be empty and is// never sent in the response.//// If modtime is not the zero time or Unix epoch, ServeContent// includes it in a Last-Modified header in the response. If the// request includes an If-Modified-Since header, ServeContent uses// modtime to decide whether the content needs to be sent at all.//// The content's Seek method must work: ServeContent uses// a seek to the end of the content to determine its size.// Note that [*os.File] implements the [io.ReadSeeker] interface.//// If the caller has set w's ETag header formatted per RFC 7232, section 2.3,// ServeContent uses it to handle requests using If-Match, If-None-Match, or If-Range.//// If an error occurs when serving the request (for example, when// handling an invalid range request), ServeContent responds with an// error message. By default, ServeContent strips the Cache-Control,// Content-Encoding, ETag, and Last-Modified headers from error responses.// The GODEBUG setting httpservecontentkeepheaders=1 causes ServeContent// to preserve these headers.func ( ResponseWriter, *Request, string, time.Time, io.ReadSeeker) { := func() (int64, error) { , := .Seek(0, io.SeekEnd)if != nil {return0, errSeeker } _, = .Seek(0, io.SeekStart)if != nil {return0, errSeeker }return , nil }serveContent(, , , , , )}// errSeeker is returned by ServeContent's sizeFunc when the content// doesn't seek properly. The underlying Seeker's error text isn't// included in the sizeFunc reply so it's not sent over HTTP to end// users.var errSeeker = errors.New("seeker can't seek")// errNoOverlap is returned by serveContent's parseRange if first-byte-pos of// all of the byte-range-spec values is greater than the content size.var errNoOverlap = errors.New("invalid range: failed to overlap")// if name is empty, filename is unknown. (used for mime type, before sniffing)// if modtime.IsZero(), modtime is unknown.// content must be seeked to the beginning of the file.// The sizeFunc is called at most once. Its error, if any, is sent in the HTTP response.func serveContent( ResponseWriter, *Request, string, time.Time, func() (int64, error), io.ReadSeeker) {setLastModified(, ) , := checkPreconditions(, , )if {return } := StatusOK// If Content-Type isn't set, use the file's extension to find it, but // if the Content-Type is unset explicitly, do not sniff the type. , := .Header()["Content-Type"]varstringif ! { = mime.TypeByExtension(filepath.Ext())if == "" {// read a chunk to decide between utf-8 text and binaryvar [sniffLen]byte , := io.ReadFull(, [:]) = DetectContentType([:]) , := .Seek(0, io.SeekStart) // rewind to output whole fileif != nil {serveError(, "seeker can't seek", StatusInternalServerError)return } } .Header().Set("Content-Type", ) } elseiflen() > 0 { = [0] } , := ()if != nil {serveError(, .Error(), StatusInternalServerError)return }if < 0 {// Should never happen but just to be sureserveError(, "negative content size computed", StatusInternalServerError)return }// handle Content-Range header. := vario.Reader = , := parseRange(, )switch {casenil:caseerrNoOverlap:if == 0 {// Some clients add a Range header to all requests to // limit the size of the response. If the file is empty, // ignore the range header and respond with a 200 rather // than a 416. = nilbreak } .Header().Set("Content-Range", fmt.Sprintf("bytes */%d", ))fallthroughdefault:serveError(, .Error(), StatusRequestedRangeNotSatisfiable)return }ifsumRangesSize() > {// The total number of bytes in all the ranges // is larger than the size of the file by // itself, so this is probably an attack, or a // dumb client. Ignore the range request. = nil }switch {caselen() == 1:// RFC 7233, Section 4.1: // "If a single part is being transferred, the server // generating the 206 response MUST generate a // Content-Range header field, describing what range // of the selected representation is enclosed, and a // payload consisting of the range. // ... // A server MUST NOT generate a multipart response to // a request for a single range, since a client that // does not request multiple parts might not support // multipart responses." := [0]if , := .Seek(.start, io.SeekStart); != nil {serveError(, .Error(), StatusRequestedRangeNotSatisfiable)return } = .length = StatusPartialContent .Header().Set("Content-Range", .contentRange())caselen() > 1: = rangesMIMESize(, , ) = StatusPartialContent , := io.Pipe() := multipart.NewWriter() .Header().Set("Content-Type", "multipart/byteranges; boundary="+.Boundary()) = defer .Close() // cause writing goroutine to fail and exit if CopyN doesn't finish.gofunc() {for , := range { , := .CreatePart(.mimeHeader(, ))if != nil { .CloseWithError()return }if , := .Seek(.start, io.SeekStart); != nil { .CloseWithError()return }if , := io.CopyN(, , .length); != nil { .CloseWithError()return } } .Close() .Close() }() } .Header().Set("Accept-Ranges", "bytes")// We should be able to unconditionally set the Content-Length here. // // However, there is a pattern observed in the wild that this breaks: // The user wraps the ResponseWriter in one which gzips data written to it, // and sets "Content-Encoding: gzip". // // The user shouldn't be doing this; the serveContent path here depends // on serving seekable data with a known length. If you want to compress // on the fly, then you shouldn't be using ServeFile/ServeContent, or // you should compress the entire file up-front and provide a seekable // view of the compressed data. // // However, since we've observed this pattern in the wild, and since // setting Content-Length here breaks code that mostly-works today, // skip setting Content-Length if the user set Content-Encoding. // // If this is a range request, always set Content-Length. // If the user isn't changing the bytes sent in the ResponseWrite, // the Content-Length will be correct. // If the user is changing the bytes sent, then the range request wasn't // going to work properly anyway and we aren't worse off. // // A possible future improvement on this might be to look at the type // of the ResponseWriter, and always set Content-Length if it's one // that we recognize.iflen() > 0 || .Header().Get("Content-Encoding") == "" { .Header().Set("Content-Length", strconv.FormatInt(, 10)) } .WriteHeader()if .Method != "HEAD" {io.CopyN(, , ) }}// scanETag determines if a syntactically valid ETag is present at s. If so,// the ETag and remaining text after consuming ETag is returned. Otherwise,// it returns "", "".func scanETag( string) ( string, string) { = textproto.TrimString() := 0ifstrings.HasPrefix(, "W/") { = 2 }iflen([:]) < 2 || [] != '"' {return"", "" }// ETag is either W/"text" or "text". // See RFC 7232 2.3.for := + 1; < len(); ++ { := []switch {// Character values allowed in ETags.case == 0x21 || >= 0x23 && <= 0x7E || >= 0x80:case == '"':return [:+1], [+1:]default:return"", "" } }return"", ""}// etagStrongMatch reports whether a and b match using strong ETag comparison.// Assumes a and b are valid ETags.func etagStrongMatch(, string) bool {return == && != "" && [0] == '"'}// etagWeakMatch reports whether a and b match using weak ETag comparison.// Assumes a and b are valid ETags.func etagWeakMatch(, string) bool {returnstrings.TrimPrefix(, "W/") == strings.TrimPrefix(, "W/")}// condResult is the result of an HTTP request precondition check.// See https://tools.ietf.org/html/rfc7232 section 3.type condResult intconst ( condNone condResult = iota condTrue condFalse)func checkIfMatch( ResponseWriter, *Request) condResult { := .Header.Get("If-Match")if == "" {returncondNone }for { = textproto.TrimString()iflen() == 0 {break }if [0] == ',' { = [1:]continue }if [0] == '*' {returncondTrue } , := scanETag()if == "" {break }ifetagStrongMatch(, .Header().get("Etag")) {returncondTrue } = }returncondFalse}func checkIfUnmodifiedSince( *Request, time.Time) condResult { := .Header.Get("If-Unmodified-Since")if == "" || isZeroTime() {returncondNone } , := ParseTime()if != nil {returncondNone }// The Last-Modified header truncates sub-second precision so // the modtime needs to be truncated too. = .Truncate(time.Second)if := .Compare(); <= 0 {returncondTrue }returncondFalse}func checkIfNoneMatch( ResponseWriter, *Request) condResult { := .Header.get("If-None-Match")if == "" {returncondNone } := for { = textproto.TrimString()iflen() == 0 {break }if [0] == ',' { = [1:]continue }if [0] == '*' {returncondFalse } , := scanETag()if == "" {break }ifetagWeakMatch(, .Header().get("Etag")) {returncondFalse } = }returncondTrue}func checkIfModifiedSince( *Request, time.Time) condResult {if .Method != "GET" && .Method != "HEAD" {returncondNone } := .Header.Get("If-Modified-Since")if == "" || isZeroTime() {returncondNone } , := ParseTime()if != nil {returncondNone }// The Last-Modified header truncates sub-second precision so // the modtime needs to be truncated too. = .Truncate(time.Second)if := .Compare(); <= 0 {returncondFalse }returncondTrue}func checkIfRange( ResponseWriter, *Request, time.Time) condResult {if .Method != "GET" && .Method != "HEAD" {returncondNone } := .Header.get("If-Range")if == "" {returncondNone } , := scanETag()if != "" {ifetagStrongMatch(, .Header().Get("Etag")) {returncondTrue } else {returncondFalse } }// The If-Range value is typically the ETag value, but it may also be // the modtime date. See golang.org/issue/8367.if .IsZero() {returncondFalse } , := ParseTime()if != nil {returncondFalse }if .Unix() == .Unix() {returncondTrue }returncondFalse}var unixEpochTime = time.Unix(0, 0)// isZeroTime reports whether t is obviously unspecified (either zero or Unix()=0).func isZeroTime( time.Time) bool {return .IsZero() || .Equal(unixEpochTime)}func setLastModified( ResponseWriter, time.Time) {if !isZeroTime() { .Header().Set("Last-Modified", .UTC().Format(TimeFormat)) }}func writeNotModified( ResponseWriter) {// RFC 7232 section 4.1: // a sender SHOULD NOT generate representation metadata other than the // above listed fields unless said metadata exists for the purpose of // guiding cache updates (e.g., Last-Modified might be useful if the // response does not have an ETag field). := .Header()delete(, "Content-Type")delete(, "Content-Length")delete(, "Content-Encoding")if .Get("Etag") != "" {delete(, "Last-Modified") } .WriteHeader(StatusNotModified)}// checkPreconditions evaluates request preconditions and reports whether a precondition// resulted in sending StatusNotModified or StatusPreconditionFailed.func checkPreconditions( ResponseWriter, *Request, time.Time) ( bool, string) {// This function carefully follows RFC 7232 section 6. := checkIfMatch(, )if == condNone { = checkIfUnmodifiedSince(, ) }if == condFalse { .WriteHeader(StatusPreconditionFailed)returntrue, "" }switchcheckIfNoneMatch(, ) {casecondFalse:if .Method == "GET" || .Method == "HEAD" {writeNotModified()returntrue, "" } else { .WriteHeader(StatusPreconditionFailed)returntrue, "" }casecondNone:ifcheckIfModifiedSince(, ) == condFalse {writeNotModified()returntrue, "" } } = .Header.get("Range")if != "" && checkIfRange(, , ) == condFalse { = "" }returnfalse, }// name is '/'-separated, not filepath.Separator.func serveFile( ResponseWriter, *Request, FileSystem, string, bool) {const = "/index.html"// redirect .../index.html to .../ // can't use Redirect() because that would make the path absolute, // which would be a problem running under StripPrefixifstrings.HasSuffix(.URL.Path, ) {localRedirect(, , "./")return } , := .Open()if != nil { , := toHTTPError()serveError(, , )return }defer .Close() , := .Stat()if != nil { , := toHTTPError()serveError(, , )return }if {// redirect to canonical path: / at end of directory url // r.URL.Path always begins with / := .URL.Pathif .IsDir() {if [len()-1] != '/' {localRedirect(, , path.Base()+"/")return } } elseif [len()-1] == '/' { := path.Base()if == "/" || == "." {// The FileSystem maps a path like "/" or "/./" to a file instead of a directory. := "http: attempting to traverse a non-directory"serveError(, , StatusInternalServerError)return }localRedirect(, , "../"+)return } }if .IsDir() { := .URL.Path// redirect if the directory name doesn't end in a slashif == "" || [len()-1] != '/' {localRedirect(, , path.Base()+"/")return }// use contents of index.html for directory, if present := strings.TrimSuffix(, "/") + , := .Open()if == nil {defer .Close() , := .Stat()if == nil { = = } } }// Still a directory? (we didn't find an index.html file)if .IsDir() {ifcheckIfModifiedSince(, .ModTime()) == condFalse {writeNotModified()return }setLastModified(, .ModTime())dirList(, , )return }// serveContent will check modification time := func() (int64, error) { return .Size(), nil }serveContent(, , .Name(), .ModTime(), , )}// toHTTPError returns a non-specific HTTP error message and status code// for a given non-nil error value. It's important that toHTTPError does not// actually return err.Error(), since msg and httpStatus are returned to users,// and historically Go's ServeContent always returned just "404 Not Found" for// all errors. We don't want to start leaking information in error messages.func toHTTPError( error) ( string, int) {iferrors.Is(, fs.ErrNotExist) {return"404 page not found", StatusNotFound }iferrors.Is(, fs.ErrPermission) {return"403 Forbidden", StatusForbidden }// Default:return"500 Internal Server Error", StatusInternalServerError}// localRedirect gives a Moved Permanently response.// It does not convert relative paths to absolute paths like Redirect does.func localRedirect( ResponseWriter, *Request, string) {if := .URL.RawQuery; != "" { += "?" + } .Header().Set("Location", ) .WriteHeader(StatusMovedPermanently)}// ServeFile replies to the request with the contents of the named// file or directory.//// If the provided file or directory name is a relative path, it is// interpreted relative to the current directory and may ascend to// parent directories. If the provided name is constructed from user// input, it should be sanitized before calling [ServeFile].//// As a precaution, ServeFile will reject requests where r.URL.Path// contains a ".." path element; this protects against callers who// might unsafely use [filepath.Join] on r.URL.Path without sanitizing// it and then use that filepath.Join result as the name argument.//// As another special case, ServeFile redirects any request where r.URL.Path// ends in "/index.html" to the same path, without the final// "index.html". To avoid such redirects either modify the path or// use [ServeContent].//// Outside of those two special cases, ServeFile does not use// r.URL.Path for selecting the file or directory to serve; only the// file or directory provided in the name argument is used.func ( ResponseWriter, *Request, string) {ifcontainsDotDot(.URL.Path) {// Too many programs use r.URL.Path to construct the argument to // serveFile. Reject the request under the assumption that happened // here and ".." may not be wanted. // Note that name might not contain "..", for example if code (still // incorrectly) used filepath.Join(myDir, r.URL.Path).serveError(, "invalid URL path", StatusBadRequest)return } , := filepath.Split()serveFile(, , Dir(), , false)}// ServeFileFS replies to the request with the contents// of the named file or directory from the file system fsys.// The files provided by fsys must implement [io.Seeker].//// If the provided name is constructed from user input, it should be// sanitized before calling [ServeFileFS].//// As a precaution, ServeFileFS will reject requests where r.URL.Path// contains a ".." path element; this protects against callers who// might unsafely use [filepath.Join] on r.URL.Path without sanitizing// it and then use that filepath.Join result as the name argument.//// As another special case, ServeFileFS redirects any request where r.URL.Path// ends in "/index.html" to the same path, without the final// "index.html". To avoid such redirects either modify the path or// use [ServeContent].//// Outside of those two special cases, ServeFileFS does not use// r.URL.Path for selecting the file or directory to serve; only the// file or directory provided in the name argument is used.func ( ResponseWriter, *Request, fs.FS, string) {ifcontainsDotDot(.URL.Path) {// Too many programs use r.URL.Path to construct the argument to // serveFile. Reject the request under the assumption that happened // here and ".." may not be wanted. // Note that name might not contain "..", for example if code (still // incorrectly) used filepath.Join(myDir, r.URL.Path).serveError(, "invalid URL path", StatusBadRequest)return }serveFile(, , FS(), , false)}func containsDotDot( string) bool {if !strings.Contains(, "..") {returnfalse }for , := rangestrings.FieldsFunc(, isSlashRune) {if == ".." {returntrue } }returnfalse}func isSlashRune( rune) bool { return == '/' || == '\\' }type fileHandler struct { root FileSystem}type ioFS struct { fsys fs.FS}type ioFile struct { file fs.File}func ( ioFS) ( string) (File, error) {if == "/" { = "." } else { = strings.TrimPrefix(, "/") } , := .fsys.Open()if != nil {returnnil, mapOpenError(, , '/', func( string) (fs.FileInfo, error) {returnfs.Stat(.fsys, ) }) }returnioFile{}, nil}func ( ioFile) () error { return .file.Close() }func ( ioFile) ( []byte) (int, error) { return .file.Read() }func ( ioFile) () (fs.FileInfo, error) { return .file.Stat() }var errMissingSeek = errors.New("io.File missing Seek method")var errMissingReadDir = errors.New("io.File directory missing ReadDir method")func ( ioFile) ( int64, int) (int64, error) { , := .file.(io.Seeker)if ! {return0, errMissingSeek }return .Seek(, )}func ( ioFile) ( int) ([]fs.DirEntry, error) { , := .file.(fs.ReadDirFile)if ! {returnnil, errMissingReadDir }return .ReadDir()}func ( ioFile) ( int) ([]fs.FileInfo, error) { , := .file.(fs.ReadDirFile)if ! {returnnil, errMissingReadDir }var []fs.FileInfofor { , := .ReadDir( - len())for , := range { , := .Info()if != nil {// Pretend it doesn't exist, like (*os.File).Readdir does.continue } = append(, ) }if != nil {return , }if < 0 || len() >= {break } }return , nil}// FS converts fsys to a [FileSystem] implementation,// for use with [FileServer] and [NewFileTransport].// The files provided by fsys must implement [io.Seeker].func ( fs.FS) FileSystem {returnioFS{}}// FileServer returns a handler that serves HTTP requests// with the contents of the file system rooted at root.//// As a special case, the returned file server redirects any request// ending in "/index.html" to the same path, without the final// "index.html".//// To use the operating system's file system implementation,// use [http.Dir]://// http.Handle("/", http.FileServer(http.Dir("/tmp")))//// To use an [fs.FS] implementation, use [http.FileServerFS] instead.func ( FileSystem) Handler {return &fileHandler{}}// FileServerFS returns a handler that serves HTTP requests// with the contents of the file system fsys.// The files provided by fsys must implement [io.Seeker].//// As a special case, the returned file server redirects any request// ending in "/index.html" to the same path, without the final// "index.html".//// http.Handle("/", http.FileServerFS(fsys))func ( fs.FS) Handler {returnFileServer(FS())}func ( *fileHandler) ( ResponseWriter, *Request) { := .URL.Pathif !strings.HasPrefix(, "/") { = "/" + .URL.Path = }serveFile(, , .root, path.Clean(), true)}// httpRange specifies the byte range to be sent to the client.type httpRange struct { start, length int64}func ( httpRange) ( int64) string {returnfmt.Sprintf("bytes %d-%d/%d", .start, .start+.length-1, )}func ( httpRange) ( string, int64) textproto.MIMEHeader {returntextproto.MIMEHeader{"Content-Range": {.contentRange()},"Content-Type": {}, }}// parseRange parses a Range header string as per RFC 7233.// errNoOverlap is returned if none of the ranges overlap.func parseRange( string, int64) ([]httpRange, error) {if == "" {returnnil, nil// header not present }const = "bytes="if !strings.HasPrefix(, ) {returnnil, errors.New("invalid range") }var []httpRange := falsefor , := rangestrings.Split([len():], ",") { = textproto.TrimString()if == "" {continue } , , := strings.Cut(, "-")if ! {returnnil, errors.New("invalid range") } , = textproto.TrimString(), textproto.TrimString()varhttpRangeif == "" {// If no start is specified, end specifies the // range start relative to the end of the file, // and we are dealing with <suffix-length> // which has to be a non-negative integer as per // RFC 7233 Section 2.1 "Byte-Ranges".if == "" || [0] == '-' {returnnil, errors.New("invalid range") } , := strconv.ParseInt(, 10, 64)if < 0 || != nil {returnnil, errors.New("invalid range") }if > { = } .start = - .length = - .start } else { , := strconv.ParseInt(, 10, 64)if != nil || < 0 {returnnil, errors.New("invalid range") }if >= {// If the range begins after the size of the content, // then it does not overlap. = truecontinue } .start = if == "" {// If no end is specified, range extends to end of the file. .length = - .start } else { , := strconv.ParseInt(, 10, 64)if != nil || .start > {returnnil, errors.New("invalid range") }if >= { = - 1 } .length = - .start + 1 } } = append(, ) }if && len() == 0 {// The specified ranges did not overlap with the content.returnnil, errNoOverlap }return , nil}// countingWriter counts how many bytes have been written to it.type countingWriter int64func ( *countingWriter) ( []byte) ( int, error) { * += countingWriter(len())returnlen(), nil}// rangesMIMESize returns the number of bytes it takes to encode the// provided ranges as a multipart response.func rangesMIMESize( []httpRange, string, int64) ( int64) {varcountingWriter := multipart.NewWriter(&)for , := range { .CreatePart(.mimeHeader(, )) += .length } .Close() += int64()return}func sumRangesSize( []httpRange) ( int64) {for , := range { += .length }return}
The pages are generated with Goldsv0.6.9-preview. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @Go100and1 (reachable from the left QR code) to get the latest news of Golds.