diff options
author | Russ Cox <rsc@golang.org> | 2020-07-07 09:51:45 -0400 |
---|---|---|
committer | Russ Cox <rsc@golang.org> | 2020-10-20 18:41:15 +0000 |
commit | 7211694a1e3f9eaebff7074944feead968e00e72 (patch) | |
tree | c49b8d6eb5a6bc1cf766076fb00c61f897d9ec28 /src/net | |
parent | 2a9aa4dcac0c33f7fffefb94a1bc92a17fd7cfd3 (diff) | |
download | go-7211694a1e3f9eaebff7074944feead968e00e72.tar.gz go-7211694a1e3f9eaebff7074944feead968e00e72.zip |
net/http: add FS to convert fs.FS to FileSystem
Two different functions in the http API expect a FileSystem:
http.FileSystem and http.NewFileTransport.
Add a general converter http.FS to turn an fs.FS into an http.FileSystem
for use with either of these functions.
(The original plan was to add http.HandlerFS taking an fs.FS directly,
but that doesn't help with NewFileTransport.)
For #41190.
Change-Id: I5f242eafe9b963f4387419a2615bdb487c358f16
Reviewed-on: https://go-review.googlesource.com/c/go/+/243939
Trust: Russ Cox <rsc@golang.org>
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Rob Pike <r@golang.org>
Diffstat (limited to 'src/net')
-rw-r--r-- | src/net/http/fs.go | 133 | ||||
-rw-r--r-- | src/net/http/fs_test.go | 37 |
2 files changed, 162 insertions, 8 deletions
diff --git a/src/net/http/fs.go b/src/net/http/fs.go index 0743b5b621..a28ae85958 100644 --- a/src/net/http/fs.go +++ b/src/net/http/fs.go @@ -87,6 +87,10 @@ func (d Dir) Open(name string) (File, error) { // 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. type FileSystem interface { Open(name string) (File, error) } @@ -103,20 +107,52 @@ type File interface { Stat() (fs.FileInfo, error) } +type anyDirs interface { + len() int + name(i int) string + isDir(i int) bool +} + +type fileInfoDirs []fs.FileInfo + +func (d fileInfoDirs) len() int { return len(d) } +func (d fileInfoDirs) isDir(i int) bool { return d[i].IsDir() } +func (d fileInfoDirs) name(i int) string { return d[i].Name() } + +type dirEntryDirs []fs.DirEntry + +func (d dirEntryDirs) len() int { return len(d) } +func (d dirEntryDirs) isDir(i int) bool { return d[i].IsDir() } +func (d dirEntryDirs) name(i int) string { return d[i].Name() } + func dirList(w ResponseWriter, r *Request, f File) { - dirs, err := f.Readdir(-1) + // Prefer to use ReadDir instead of Readdir, + // because the former doesn't require calling + // Stat on every entry of a directory on Unix. + var dirs anyDirs + var err error + if d, ok := f.(fs.ReadDirFile); ok { + var list dirEntryDirs + list, err = d.ReadDir(-1) + dirs = list + } else { + var list fileInfoDirs + list, err = f.Readdir(-1) + dirs = list + } + if err != nil { logf(r, "http: error reading directory: %v", err) Error(w, "Error reading directory", StatusInternalServerError) return } - sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() }) + sort.Slice(dirs, func(i, j int) bool { return dirs.name(i) < dirs.name(j) }) w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, "<pre>\n") - for _, d := range dirs { - name := d.Name() - if d.IsDir() { + for i, n := 0, dirs.len(); i < n; i++ { + name := dirs.name(i) + if dirs.isDir(i) { name += "/" } // name may contain '?' or '#', which must be escaped to remain @@ -707,17 +743,98 @@ type fileHandler struct { root FileSystem } +type ioFS struct { + fsys fs.FS +} + +type ioFile struct { + file fs.File +} + +func (f ioFS) Open(name string) (File, error) { + if name == "/" { + name = "." + } else { + name = strings.TrimPrefix(name, "/") + } + file, err := f.fsys.Open(name) + if err != nil { + return nil, err + } + return ioFile{file}, nil +} + +func (f ioFile) Close() error { return f.file.Close() } +func (f ioFile) Read(b []byte) (int, error) { return f.file.Read(b) } +func (f ioFile) Stat() (fs.FileInfo, error) { return f.file.Stat() } + +var errMissingSeek = errors.New("io.File missing Seek method") +var errMissingReadDir = errors.New("io.File directory missing ReadDir method") + +func (f ioFile) Seek(offset int64, whence int) (int64, error) { + s, ok := f.file.(io.Seeker) + if !ok { + return 0, errMissingSeek + } + return s.Seek(offset, whence) +} + +func (f ioFile) ReadDir(count int) ([]fs.DirEntry, error) { + d, ok := f.file.(fs.ReadDirFile) + if !ok { + return nil, errMissingReadDir + } + return d.ReadDir(count) +} + +func (f ioFile) Readdir(count int) ([]fs.FileInfo, error) { + d, ok := f.file.(fs.ReadDirFile) + if !ok { + return nil, errMissingReadDir + } + var list []fs.FileInfo + for { + dirs, err := d.ReadDir(count - len(list)) + for _, dir := range dirs { + info, err := dir.Info() + if err != nil { + // Pretend it doesn't exist, like (*os.File).Readdir does. + continue + } + list = append(list, info) + } + if err != nil { + return list, err + } + if count < 0 || len(list) >= count { + break + } + } + return list, nil +} + +// FS converts fsys to a FileSystem implementation, +// for use with FileServer and NewFileTransport. +func FS(fsys fs.FS) FileSystem { + return ioFS{fsys} +} + // 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"))) // -// 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 an fs.FS implementation, use http.FS to convert it: +// +// http.Handle("/", http.FileServer(http.FS(fsys))) +// func FileServer(root FileSystem) Handler { return &fileHandler{root} } diff --git a/src/net/http/fs_test.go b/src/net/http/fs_test.go index de793b331e..c9f324cff6 100644 --- a/src/net/http/fs_test.go +++ b/src/net/http/fs_test.go @@ -571,6 +571,43 @@ func testServeFileWithContentEncoding(t *testing.T, h2 bool) { func TestServeIndexHtml(t *testing.T) { defer afterTest(t) + + for i := 0; i < 2; i++ { + var h Handler + var name string + switch i { + case 0: + h = FileServer(Dir(".")) + name = "Dir" + case 1: + h = FileServer(FS(os.DirFS("."))) + name = "DirFS" + } + t.Run(name, func(t *testing.T) { + const want = "index.html says hello\n" + ts := httptest.NewServer(h) + defer ts.Close() + + for _, path := range []string{"/testdata/", "/testdata/index.html"} { + res, err := Get(ts.URL + path) + if err != nil { + t.Fatal(err) + } + b, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatal("reading Body:", err) + } + if s := string(b); s != want { + t.Errorf("for path %q got %q, want %q", path, s, want) + } + res.Body.Close() + } + }) + } +} + +func TestServeIndexHtmlFS(t *testing.T) { + defer afterTest(t) const want = "index.html says hello\n" ts := httptest.NewServer(FileServer(Dir("."))) defer ts.Close() |