The Go Programming Language

Source file src/pkg/http/fs.go

     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		"fmt"
    11		"io"
    12		"mime"
    13		"os"
    14		"path"
    15		"path/filepath"
    16		"strconv"
    17		"strings"
    18		"time"
    19		"utf8"
    20	)
    21	
    22	// A Dir implements http.FileSystem using the native file
    23	// system restricted to a specific directory tree.
    24	type Dir string
    25	
    26	func (d Dir) Open(name string) (File, os.Error) {
    27		if filepath.Separator != '/' && strings.IndexRune(name, filepath.Separator) >= 0 {
    28			return nil, os.NewError("http: invalid character in file path")
    29		}
    30		f, err := os.Open(filepath.Join(string(d), filepath.FromSlash(path.Clean("/"+name))))
    31		if err != nil {
    32			return nil, err
    33		}
    34		return f, nil
    35	}
    36	
    37	// A FileSystem implements access to a collection of named files.
    38	// The elements in a file path are separated by slash ('/', U+002F)
    39	// characters, regardless of host operating system convention.
    40	type FileSystem interface {
    41		Open(name string) (File, os.Error)
    42	}
    43	
    44	// A File is returned by a FileSystem's Open method and can be
    45	// served by the FileServer implementation.
    46	type File interface {
    47		Close() os.Error
    48		Stat() (*os.FileInfo, os.Error)
    49		Readdir(count int) ([]os.FileInfo, os.Error)
    50		Read([]byte) (int, os.Error)
    51		Seek(offset int64, whence int) (int64, os.Error)
    52	}
    53	
    54	// Heuristic: b is text if it is valid UTF-8 and doesn't
    55	// contain any unprintable ASCII or Unicode characters.
    56	func isText(b []byte) bool {
    57		for len(b) > 0 && utf8.FullRune(b) {
    58			rune, size := utf8.DecodeRune(b)
    59			if size == 1 && rune == utf8.RuneError {
    60				// decoding error
    61				return false
    62			}
    63			if 0x7F <= rune && rune <= 0x9F {
    64				return false
    65			}
    66			if rune < ' ' {
    67				switch rune {
    68				case '\n', '\r', '\t':
    69					// okay
    70				default:
    71					// binary garbage
    72					return false
    73				}
    74			}
    75			b = b[size:]
    76		}
    77		return true
    78	}
    79	
    80	func dirList(w ResponseWriter, f File) {
    81		w.Header().Set("Content-Type", "text/html; charset=utf-8")
    82		fmt.Fprintf(w, "<pre>\n")
    83		for {
    84			dirs, err := f.Readdir(100)
    85			if err != nil || len(dirs) == 0 {
    86				break
    87			}
    88			for _, d := range dirs {
    89				name := d.Name
    90				if d.IsDirectory() {
    91					name += "/"
    92				}
    93				// TODO htmlescape
    94				fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", name, name)
    95			}
    96		}
    97		fmt.Fprintf(w, "</pre>\n")
    98	}
    99	
   100	// name is '/'-separated, not filepath.Separator.
   101	func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirect bool) {
   102		const indexPage = "/index.html"
   103	
   104		// redirect .../index.html to .../
   105		// can't use Redirect() because that would make the path absolute,
   106		// which would be a problem running under StripPrefix
   107		if strings.HasSuffix(r.URL.Path, indexPage) {
   108			localRedirect(w, r, "./")
   109			return
   110		}
   111	
   112		f, err := fs.Open(name)
   113		if err != nil {
   114			// TODO expose actual error?
   115			NotFound(w, r)
   116			return
   117		}
   118		defer f.Close()
   119	
   120		d, err1 := f.Stat()
   121		if err1 != nil {
   122			// TODO expose actual error?
   123			NotFound(w, r)
   124			return
   125		}
   126	
   127		if redirect {
   128			// redirect to canonical path: / at end of directory url
   129			// r.URL.Path always begins with /
   130			url := r.URL.Path
   131			if d.IsDirectory() {
   132				if url[len(url)-1] != '/' {
   133					localRedirect(w, r, path.Base(url)+"/")
   134					return
   135				}
   136			} else {
   137				if url[len(url)-1] == '/' {
   138					localRedirect(w, r, "../"+path.Base(url))
   139					return
   140				}
   141			}
   142		}
   143	
   144		if t, _ := time.Parse(TimeFormat, r.Header.Get("If-Modified-Since")); t != nil && d.Mtime_ns/1e9 <= t.Seconds() {
   145			w.WriteHeader(StatusNotModified)
   146			return
   147		}
   148		w.Header().Set("Last-Modified", time.SecondsToUTC(d.Mtime_ns/1e9).Format(TimeFormat))
   149	
   150		// use contents of index.html for directory, if present
   151		if d.IsDirectory() {
   152			index := name + indexPage
   153			ff, err := fs.Open(index)
   154			if err == nil {
   155				defer ff.Close()
   156				dd, err := ff.Stat()
   157				if err == nil {
   158					name = index
   159					d = dd
   160					f = ff
   161				}
   162			}
   163		}
   164	
   165		if d.IsDirectory() {
   166			dirList(w, f)
   167			return
   168		}
   169	
   170		// serve file
   171		size := d.Size
   172		code := StatusOK
   173	
   174		// If Content-Type isn't set, use the file's extension to find it.
   175		if w.Header().Get("Content-Type") == "" {
   176			ctype := mime.TypeByExtension(filepath.Ext(name))
   177			if ctype == "" {
   178				// read a chunk to decide between utf-8 text and binary
   179				var buf [1024]byte
   180				n, _ := io.ReadFull(f, buf[:])
   181				b := buf[:n]
   182				if isText(b) {
   183					ctype = "text/plain; charset=utf-8"
   184				} else {
   185					// generic binary
   186					ctype = "application/octet-stream"
   187				}
   188				f.Seek(0, os.SEEK_SET) // rewind to output whole file
   189			}
   190			w.Header().Set("Content-Type", ctype)
   191		}
   192	
   193		// handle Content-Range header.
   194		// TODO(adg): handle multiple ranges
   195		ranges, err := parseRange(r.Header.Get("Range"), size)
   196		if err == nil && len(ranges) > 1 {
   197			err = os.NewError("multiple ranges not supported")
   198		}
   199		if err != nil {
   200			Error(w, err.String(), StatusRequestedRangeNotSatisfiable)
   201			return
   202		}
   203		if len(ranges) == 1 {
   204			ra := ranges[0]
   205			if _, err := f.Seek(ra.start, os.SEEK_SET); err != nil {
   206				Error(w, err.String(), StatusRequestedRangeNotSatisfiable)
   207				return
   208			}
   209			size = ra.length
   210			code = StatusPartialContent
   211			w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", ra.start, ra.start+ra.length-1, d.Size))
   212		}
   213	
   214		w.Header().Set("Accept-Ranges", "bytes")
   215		if w.Header().Get("Content-Encoding") == "" {
   216			w.Header().Set("Content-Length", strconv.Itoa64(size))
   217		}
   218	
   219		w.WriteHeader(code)
   220	
   221		if r.Method != "HEAD" {
   222			io.Copyn(w, f, size)
   223		}
   224	}
   225	
   226	// localRedirect gives a Moved Permanently response.
   227	// It does not convert relative paths to absolute paths like Redirect does.
   228	func localRedirect(w ResponseWriter, r *Request, newPath string) {
   229		if q := r.URL.RawQuery; q != "" {
   230			newPath += "?" + q
   231		}
   232		w.Header().Set("Location", newPath)
   233		w.WriteHeader(StatusMovedPermanently)
   234	}
   235	
   236	// ServeFile replies to the request with the contents of the named file or directory.
   237	func ServeFile(w ResponseWriter, r *Request, name string) {
   238		dir, file := filepath.Split(name)
   239		serveFile(w, r, Dir(dir), file, false)
   240	}
   241	
   242	type fileHandler struct {
   243		root FileSystem
   244	}
   245	
   246	// FileServer returns a handler that serves HTTP requests
   247	// with the contents of the file system rooted at root.
   248	//
   249	// To use the operating system's file system implementation,
   250	// use http.Dir:
   251	//
   252	//     http.Handle("/", http.FileServer(http.Dir("/tmp")))
   253	func FileServer(root FileSystem) Handler {
   254		return &fileHandler{root}
   255	}
   256	
   257	func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) {
   258		upath := r.URL.Path
   259		if !strings.HasPrefix(upath, "/") {
   260			upath = "/" + upath
   261			r.URL.Path = upath
   262		}
   263		serveFile(w, r, f.root, path.Clean(upath), true)
   264	}
   265	
   266	// httpRange specifies the byte range to be sent to the client.
   267	type httpRange struct {
   268		start, length int64
   269	}
   270	
   271	// parseRange parses a Range header string as per RFC 2616.
   272	func parseRange(s string, size int64) ([]httpRange, os.Error) {
   273		if s == "" {
   274			return nil, nil // header not present
   275		}
   276		const b = "bytes="
   277		if !strings.HasPrefix(s, b) {
   278			return nil, os.NewError("invalid range")
   279		}
   280		var ranges []httpRange
   281		for _, ra := range strings.Split(s[len(b):], ",") {
   282			i := strings.Index(ra, "-")
   283			if i < 0 {
   284				return nil, os.NewError("invalid range")
   285			}
   286			start, end := ra[:i], ra[i+1:]
   287			var r httpRange
   288			if start == "" {
   289				// If no start is specified, end specifies the
   290				// range start relative to the end of the file.
   291				i, err := strconv.Atoi64(end)
   292				if err != nil {
   293					return nil, os.NewError("invalid range")
   294				}
   295				if i > size {
   296					i = size
   297				}
   298				r.start = size - i
   299				r.length = size - r.start
   300			} else {
   301				i, err := strconv.Atoi64(start)
   302				if err != nil || i > size || i < 0 {
   303					return nil, os.NewError("invalid range")
   304				}
   305				r.start = i
   306				if end == "" {
   307					// If no end is specified, range extends to end of the file.
   308					r.length = size - r.start
   309				} else {
   310					i, err := strconv.Atoi64(end)
   311					if err != nil || r.start > i {
   312						return nil, os.NewError("invalid range")
   313					}
   314					if i >= size {
   315						i = size - 1
   316					}
   317					r.length = i - r.start + 1
   318				}
   319			}
   320			ranges = append(ranges, r)
   321		}
   322		return ranges, nil
   323	}

release.r60.3. Except as noted, this content is licensed under a Creative Commons Attribution 3.0 License.