]> Cypherpunks repositories - gostls13.git/commitdiff
net/http: add FS to convert fs.FS to FileSystem
authorRuss Cox <rsc@golang.org>
Tue, 7 Jul 2020 13:51:45 +0000 (09:51 -0400)
committerRuss Cox <rsc@golang.org>
Tue, 20 Oct 2020 18:41:15 +0000 (18:41 +0000)
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>
src/net/http/fs.go
src/net/http/fs_test.go

index 0743b5b621c6e8ae3c725c6bbc1c0e508b8eaba4..a28ae85958fe6bff917ddfbf93fe56575601d20f 100644 (file)
@@ -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}
 }
index de793b331e680e44080936c3f3e32e310f702617..c9f324cff6f63de4ad90475548e534cd6028eb49 100644 (file)
@@ -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()