mirror of
https://github.com/golang/go
synced 2024-09-15 22:20:06 +00:00
net/http: add ServeFileFS, FileServerFS, NewFileTransportFS
These new apis are analogous to ServeFile, FileServer and NewFileTransport respectively. The main difference is that these functions operate on an fs.FS.
Fixes #51971
Change-Id: Ie56b245b795eeb7edf613657578592306945469b
GitHub-Last-Rev: 26e75c0368
GitHub-Pull-Request: golang/go#61641
Reviewed-on: https://go-review.googlesource.com/c/go/+/513956
Run-TryBot: Damien Neil <dneil@google.com>
Reviewed-by: David Chase <drchase@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Damien Neil <dneil@google.com>
This commit is contained in:
parent
4e728e5121
commit
65d4723b49
3
api/next/51971.txt
Normal file
3
api/next/51971.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
pkg net/http, func ServeFileFS(ResponseWriter, *Request, fs.FS, string) #51971
|
||||
pkg net/http, func FileServerFS(fs.FS) Handler #51971
|
||||
pkg net/http, func NewFileTransportFS(fs.FS) RoundTripper #51971
|
|
@ -7,6 +7,7 @@ package http
|
|||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
// fileTransport implements RoundTripper for the 'file' protocol.
|
||||
|
@ -31,6 +32,24 @@ func NewFileTransport(fs FileSystem) RoundTripper {
|
|||
return fileTransport{fileHandler{fs}}
|
||||
}
|
||||
|
||||
// NewFileTransportFS returns a new RoundTripper, serving the provided
|
||||
// file system fsys. The returned RoundTripper ignores the URL host in its
|
||||
// incoming requests, as well as most other properties of the
|
||||
// request.
|
||||
//
|
||||
// The typical use case for NewFileTransportFS is to register the "file"
|
||||
// protocol with a Transport, as in:
|
||||
//
|
||||
// fsys := os.DirFS("/")
|
||||
// t := &http.Transport{}
|
||||
// t.RegisterProtocol("file", http.NewFileTransportFS(fsys))
|
||||
// c := &http.Client{Transport: t}
|
||||
// res, err := c.Get("file:///etc/passwd")
|
||||
// ...
|
||||
func NewFileTransportFS(fsys fs.FS) RoundTripper {
|
||||
return NewFileTransport(FS(fsys))
|
||||
}
|
||||
|
||||
func (t fileTransport) RoundTrip(req *Request) (resp *Response, err error) {
|
||||
// We start ServeHTTP in a goroutine, which may take a long
|
||||
// time if the file is large. The newPopulateResponseWriter
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
)
|
||||
|
||||
func checker(t *testing.T) func(string, error) {
|
||||
|
@ -62,3 +63,44 @@ func TestFileTransport(t *testing.T) {
|
|||
}
|
||||
res.Body.Close()
|
||||
}
|
||||
|
||||
func TestFileTransportFS(t *testing.T) {
|
||||
check := checker(t)
|
||||
|
||||
fsys := fstest.MapFS{
|
||||
"index.html": {Data: []byte("index.html says hello")},
|
||||
}
|
||||
|
||||
tr := &Transport{}
|
||||
tr.RegisterProtocol("file", NewFileTransportFS(fsys))
|
||||
c := &Client{Transport: tr}
|
||||
|
||||
for fname, mfile := range fsys {
|
||||
urlstr := "file:///" + fname
|
||||
res, err := c.Get(urlstr)
|
||||
check("Get "+urlstr, err)
|
||||
if res.StatusCode != 200 {
|
||||
t.Errorf("for %s, StatusCode = %d, want 200", urlstr, res.StatusCode)
|
||||
}
|
||||
if res.ContentLength != -1 {
|
||||
t.Errorf("for %s, ContentLength = %d, want -1", urlstr, res.ContentLength)
|
||||
}
|
||||
if res.Body == nil {
|
||||
t.Fatalf("for %s, nil Body", urlstr)
|
||||
}
|
||||
slurp, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
check("ReadAll "+urlstr, err)
|
||||
if string(slurp) != string(mfile.Data) {
|
||||
t.Errorf("for %s, got content %q, want %q", urlstr, string(slurp), "Bar")
|
||||
}
|
||||
}
|
||||
|
||||
const badURL = "file://../no-exist.txt"
|
||||
res, err := c.Get(badURL)
|
||||
check("Get "+badURL, err)
|
||||
if res.StatusCode != 404 {
|
||||
t.Errorf("for %s, StatusCode = %d, want 404", badURL, res.StatusCode)
|
||||
}
|
||||
res.Body.Close()
|
||||
}
|
||||
|
|
|
@ -741,6 +741,40 @@ func ServeFile(w ResponseWriter, r *Request, name string) {
|
|||
serveFile(w, r, Dir(dir), file, false)
|
||||
}
|
||||
|
||||
// ServeFileFS replies to the request with the contents
|
||||
// of the named file or directory from the file system fsys.
|
||||
//
|
||||
// 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 ServeFileFS(w ResponseWriter, r *Request, fsys fs.FS, name string) {
|
||||
if containsDotDot(r.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).
|
||||
Error(w, "invalid URL path", StatusBadRequest)
|
||||
return
|
||||
}
|
||||
serveFile(w, r, FS(fsys), name, false)
|
||||
}
|
||||
|
||||
func containsDotDot(v string) bool {
|
||||
if !strings.Contains(v, "..") {
|
||||
return false
|
||||
|
@ -850,13 +884,23 @@ func FS(fsys fs.FS) FileSystem {
|
|||
//
|
||||
// http.Handle("/", http.FileServer(http.Dir("/tmp")))
|
||||
//
|
||||
// To use an fs.FS implementation, use http.FS to convert it:
|
||||
//
|
||||
// http.Handle("/", http.FileServer(http.FS(fsys)))
|
||||
// To use an fs.FS implementation, use http.FileServerFS instead.
|
||||
func FileServer(root FileSystem) Handler {
|
||||
return &fileHandler{root}
|
||||
}
|
||||
|
||||
// FileServerFS returns a handler that serves HTTP requests
|
||||
// with the contents of the file system fsys.
|
||||
//
|
||||
// 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 FileServerFS(root fs.FS) Handler {
|
||||
return FileServer(FS(root))
|
||||
}
|
||||
|
||||
func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) {
|
||||
upath := r.URL.Path
|
||||
if !strings.HasPrefix(upath, "/") {
|
||||
|
|
|
@ -26,6 +26,7 @@ import (
|
|||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -1559,3 +1560,51 @@ func testFileServerMethods(t *testing.T, mode testMode) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileServerFS(t *testing.T) {
|
||||
filename := "index.html"
|
||||
contents := []byte("index.html says hello")
|
||||
fsys := fstest.MapFS{
|
||||
filename: {Data: contents},
|
||||
}
|
||||
ts := newClientServerTest(t, http1Mode, FileServerFS(fsys)).ts
|
||||
defer ts.Close()
|
||||
|
||||
res, err := ts.Client().Get(ts.URL + "/" + filename)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatal("reading Body:", err)
|
||||
}
|
||||
if s := string(b); s != string(contents) {
|
||||
t.Errorf("for path %q got %q, want %q", filename, s, contents)
|
||||
}
|
||||
res.Body.Close()
|
||||
}
|
||||
|
||||
func TestServeFileFS(t *testing.T) {
|
||||
filename := "index.html"
|
||||
contents := []byte("index.html says hello")
|
||||
fsys := fstest.MapFS{
|
||||
filename: {Data: contents},
|
||||
}
|
||||
ts := newClientServerTest(t, http1Mode, HandlerFunc(func(w ResponseWriter, r *Request) {
|
||||
ServeFileFS(w, r, fsys, filename)
|
||||
})).ts
|
||||
defer ts.Close()
|
||||
|
||||
res, err := ts.Client().Get(ts.URL + "/" + filename)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatal("reading Body:", err)
|
||||
}
|
||||
if s := string(b); s != string(contents) {
|
||||
t.Errorf("for path %q got %q, want %q", filename, s, contents)
|
||||
}
|
||||
res.Body.Close()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue