mirror of
https://github.com/caddyserver/caddy.git
synced 2026-11-11 19:37:55 +00:00
filesystems: forward optional fs interfaces (#8388)
wrapperFs embeds fs.FS, which hides the StatFS, ReadDirFS and
ReadFileFS methods of the wrapped filesystem. fs.Stat on
DefaultFileSystem did open, fstat and close instead of one stat,
for every fs.Stat in the file matcher and file_server, including the
literal-path case of fs.Glob.
GlobFS is not forwarded: OsFS.Glob uses filepath.Glob, which differs
from fs.Glob (Lstat for literal patterns, OS path separators).
Stat now succeeds on files and directories that cannot be opened, and
no longer blocks on FIFOs. The file matcher matches unreadable files
instead of skipping them, file_server picks an unreadable index file
(403) instead of trying the next one, and browse lists symlinks to
unreadable directories as directories.
BenchmarkFileMatcher, Apple M5 Pro, macOS, count=10:
before after delta
existing file 18.90µs 1.570µs -91.70%
front controller 20.85µs 3.040µs -85.42%
existing file 936 B 728 B 22 -> 18 allocs
front controller 1312 B 1520 B 35 -> 33 allocs
This commit is contained in:
1 parent
5bd0b9355d
commit
b426776cc7
3 files changed
+113
No files matched your search
@@ -18,6 +18,19 @@ type wrapperFs struct {
|
||||
fs.FS
|
||||
}
|
||||
|
||||
// Embedding fs.FS hides the wrapped FS's optional interfaces, so forward them.
|
||||
// Not GlobFS: OsFS.Glob uses filepath.Glob, whose semantics differ from fs.Glob.
|
||||
|
||||
func (w *wrapperFs) Stat(name string) (fs.FileInfo, error) { return fs.Stat(w.FS, name) }
|
||||
func (w *wrapperFs) ReadDir(name string) ([]fs.DirEntry, error) { return fs.ReadDir(w.FS, name) }
|
||||
func (w *wrapperFs) ReadFile(name string) ([]byte, error) { return fs.ReadFile(w.FS, name) }
|
||||
|
||||
var (
|
||||
_ fs.StatFS = (*wrapperFs)(nil)
|
||||
_ fs.ReadDirFS = (*wrapperFs)(nil)
|
||||
_ fs.ReadFileFS = (*wrapperFs)(nil)
|
||||
)
|
||||
|
||||
// FileSystemMap stores a map of filesystems
|
||||
// the empty key will be overwritten to be the default key
|
||||
// it includes a default filesystem, based off the os fs
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package filesystems
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
)
|
||||
|
||||
type openCountingFS struct {
|
||||
fstest.MapFS
|
||||
opens int
|
||||
}
|
||||
|
||||
func (o *openCountingFS) Open(name string) (fs.File, error) {
|
||||
o.opens++
|
||||
return o.MapFS.Open(name)
|
||||
}
|
||||
|
||||
func TestWrapperFsStat(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
hideStatFS bool
|
||||
wantOpens int
|
||||
}{
|
||||
{hideStatFS: false, wantOpens: 0},
|
||||
{hideStatFS: true, wantOpens: 1},
|
||||
} {
|
||||
inner := &openCountingFS{MapFS: fstest.MapFS{"file": {Data: []byte("data")}}}
|
||||
var fsys fs.FS = inner
|
||||
if tc.hideStatFS {
|
||||
fsys = struct{ fs.FS }{inner}
|
||||
}
|
||||
var m FileSystemMap
|
||||
m.Register("test", fsys)
|
||||
wrapped, _ := m.Get("test")
|
||||
|
||||
info, err := fs.Stat(wrapped, "file")
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: unexpected error: %v", i, err)
|
||||
}
|
||||
if info.Size() != 4 {
|
||||
t.Errorf("Test %d: expected size 4, got %d", i, info.Size())
|
||||
}
|
||||
if inner.opens != tc.wantOpens {
|
||||
t.Errorf("Test %d: expected %d opens, got %d", i, tc.wantOpens, inner.opens)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultFileSystemStatDoesNotOpen(t *testing.T) {
|
||||
if runtime.GOOS == "windows" || os.Geteuid() == 0 {
|
||||
t.Skip("needs a file that cannot be opened")
|
||||
}
|
||||
name := filepath.Join(t.TempDir(), "unreadable")
|
||||
if err := os.WriteFile(name, []byte("data"), 0o000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := DefaultFileSystem.Open(name); err == nil {
|
||||
t.Fatal("expected open to fail")
|
||||
}
|
||||
|
||||
info, err := fs.Stat(DefaultFileSystem, name)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if info.Size() != 4 {
|
||||
t.Errorf("expected size 4, got %d", info.Size())
|
||||
}
|
||||
}
|
||||
@@ -870,3 +870,32 @@ func TestSizePoliciesWithEmptyFiles(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFileMatcher(b *testing.B) {
|
||||
for _, bc := range []struct {
|
||||
name string
|
||||
path string
|
||||
}{
|
||||
{name: "existing file", path: "/index.php"},
|
||||
{name: "front controller", path: "/blog/my-post"},
|
||||
} {
|
||||
b.Run(bc.name, func(b *testing.B) {
|
||||
m := &MatchFile{
|
||||
fsmap: &filesystems.FileSystemMap{},
|
||||
Root: "./testdata",
|
||||
TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php", "index.php"},
|
||||
TryPolicy: tryPolicyFirstExist,
|
||||
SplitPath: []string{".php"},
|
||||
}
|
||||
req := &http.Request{URL: &url.URL{Path: bc.path}}
|
||||
caddyhttp.NewTestReplacer(req)
|
||||
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
if ok, err := m.MatchWithError(req); !ok || err != nil {
|
||||
b.Fatalf("expected match, got %t, %v", ok, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user