Repository: gobuffalo/buffalo
Pull request #2179 (2021-11-27 14:29:49)

v0.18.1


Patch:
diff --git a/fs.go b/fs.go
new file mode 100644
index 00000000..e7436c3c
--- /dev/null
+++ b/fs.go
@@ -0,0 +1,76 @@
+package buffalo
+
+import (
+	"fmt"
+	"io/fs"
+	"os"
+)
+
+// FS wraps a directory and an embed FS that are expected to have the same contents.
+// it prioritizes the directory FS and falls back to the embedded FS if the file cannot
+// be found on disk. This is useful during development or when deploying with
+// assets not embedded in the binary.
+//
+// Additionally FS hiddes any file named embed.go from the FS.
+type FS struct {
+	embed fs.FS
+	dir   fs.FS
+}
+
+// NewFS returns a new FS that wraps the given directory and embedded FS.
+// the embed.FS is expected to embed the same files as the directory FS.
+func NewFS(embed fs.ReadDirFS, dir string) FS {
+	return FS{
+		embed: embed,
+		dir:   os.DirFS(dir),
+	}
+}
+
+// Open implements the FS interface.
+func (f FS) Open(name string) (fs.File, error) {
+	if name == "embed.go" {
+		return nil, fs.ErrNotExist
+	}
+	file, err := f.getFile(name)
+	if name == "." {
+		return rootFile{file}, err
+	}
+	return file, err
+}
+
+func (f FS) getFile(name string) (fs.File, error) {
+	file, err := f.dir.Open(name)
+	if err == nil {
+		return file, nil
+	}
+
+	return f.embed.Open(name)
+}
+
+// rootFile wraps the "." directory for hidding the embed.go file.
+type rootFile struct {
+	fs.File
+}
+
+// ReadDir implements the fs.ReadDirFile interface.
+func (f rootFile) ReadDir(n int) (entries []fs.DirEntry, err error) {
+	dir, ok := f.File.(fs.ReadDirFile)
+	if !ok {
+		return nil, fmt.Errorf("%T is not a directory", f.File)
+	}
+
+	entries, err = dir.ReadDir(n)
+	entries = hideEmbedFile(entries)
+	return entries, err
+}
+
+func hideEmbedFile(entries []fs.DirEntry) []fs.DirEntry {
+	result := make([]fs.DirEntry, 0, len(entries))
+
+	for _, entry := range entries {
+		if entry.Name() != "embed.go" {
+			result = append(result, entry)
+		}
+	}
+	return result
+}
diff --git a/render/html.go b/render/html.go
index 48d93255..0b54af4c 100644
--- a/render/html.go
+++ b/render/html.go
@@ -2,6 +2,7 @@ package render
 
 import (
 	"html"
+	"strings"
 
 	"github.com/gobuffalo/github_flavored_markdown"
 	"github.com/gobuffalo/plush/v4"
@@ -28,6 +29,14 @@ func HTML(names ...string) Renderer {
 // in the options, then that layout file will be used
 // automatically.
 func (e *Engine) HTML(names ...string) Renderer {
+	// just allow leading slash and remove them here.
+	// generated actions were various by buffalo versions.
+	tmp := []string{}
+	for _, name := range names {
+		tmp = append(tmp, strings.TrimPrefix(name, "/"))
+	}
+	names = tmp
+
 	if e.HTMLLayout != "" && len(names) == 1 {
 		names = append(names, e.HTMLLayout)
 	}
diff --git a/runtime/version.go b/runtime/version.go
index 2eca1b0c..dcec814e 100644
--- a/runtime/version.go
+++ b/runtime/version.go
@@ -1,4 +1,4 @@
 package runtime
 
 // Version is the current version of the buffalo binary
-var Version = "v0.18.0"
+var Version = "v0.18.1"


Test patch:
diff --git a/fs_test.go b/fs_test.go
new file mode 100644
index 00000000..11d14276
--- /dev/null
+++ b/fs_test.go
@@ -0,0 +1,100 @@
+package buffalo
+
+import (
+	"io"
+	"io/fs"
+	"testing"
+
+	"github.com/gobuffalo/buffalo/internal/testdata/embedded"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_FS_Disallows_Parent_Folders(t *testing.T) {
+	r := require.New(t)
+
+	fsys := NewFS(embedded.FS(), "internal/testdata/disk")
+	r.NotNil(fsys)
+
+	f, err := fsys.Open("../panic.txt")
+	r.ErrorIs(err, fs.ErrNotExist)
+	r.Nil(f)
+
+	f, err = fsys.Open("try/../to/../trick/../panic.txt")
+	r.ErrorIs(err, fs.ErrNotExist)
+	r.Nil(f)
+}
+
+func Test_FS_Hides_embed_go(t *testing.T) {
+	r := require.New(t)
+
+	fsys := NewFS(embedded.FS(), "internal/testdata/disk")
+	r.NotNil(fsys)
+
+	f, err := fsys.Open("embed.go")
+	r.ErrorIs(err, fs.ErrNotExist)
+	r.Nil(f)
+}
+
+func Test_FS_Prioritizes_Disk(t *testing.T) {
+	r := require.New(t)
+
+	fsys := NewFS(embedded.FS(), "internal/testdata/disk")
+	r.NotNil(fsys)
+
+	f, err := fsys.Open("file.txt")
+	r.NoError(err)
+
+	b, err := io.ReadAll(f)
+	r.NoError(err)
+
+	r.Equal("This file is on disk.", string(b))
+}
+
+func Test_FS_Uses_Embed_If_No_Disk(t *testing.T) {
+	r := require.New(t)
+
+	fsys := NewFS(embedded.FS(), "internal/testdata/empty")
+	r.NotNil(fsys)
+
+	f, err := fsys.Open("file.txt")
+	r.NoError(err)
+
+	b, err := io.ReadAll(f)
+	r.NoError(err)
+
+	r.Equal("This file is embedded.", string(b))
+}
+
+func Test_FS_ReadDirFile(t *testing.T) {
+	r := require.New(t)
+
+	fsys := NewFS(embedded.FS(), "internal/testdata/disk")
+	r.NotNil(fsys)
+
+	f, err := fsys.Open(".")
+	r.NoError(err)
+
+	dir, ok := f.(fs.ReadDirFile)
+	r.True(ok, "folder does not implement fs.ReadDirFile interface")
+
+	// First read should return at most 1 file
+	entries, err := dir.ReadDir(1)
+	r.NoError(err)
+
+	// The actual len will be 0 because the first file read is the embed.go file
+	// this is counter-intuitive, but it's how the fs.ReadDirFile interface is specified;
+	// if err == nil, just continue to call ReadDir until io.EOF is returned.
+	r.LessOrEqual(len(entries), 1, "a call to ReadDir must at most return n entries")
+
+	// Second read should return at most 2 files
+	entries, err = dir.ReadDir(2)
+	r.NoError(err)
+
+	// The actual len will be 2 (file.txt & file2.txt)
+	r.LessOrEqual(len(entries), 2, "a call to ReadDir must at most return n entries")
+
+	// trying to read next 2 files (none left)
+	entries, err = dir.ReadDir(2)
+	r.ErrorIs(err, io.EOF)
+	r.Empty(entries)
+}
diff --git a/internal/testdata/disk/embed.go b/internal/testdata/disk/embed.go
new file mode 100644
index 00000000..e69de29b
diff --git a/internal/testdata/disk/file.txt b/internal/testdata/disk/file.txt
new file mode 100644
index 00000000..e286859c
--- /dev/null
+++ b/internal/testdata/disk/file.txt
@@ -0,0 +1,1 @@
+This file is on disk.
\ No newline at end of file
diff --git a/internal/testdata/disk/file2.txt b/internal/testdata/disk/file2.txt
new file mode 100644
index 00000000..e69de29b
diff --git a/internal/testdata/embedded/embed.go b/internal/testdata/embedded/embed.go
new file mode 100644
index 00000000..c6f4a841
--- /dev/null
+++ b/internal/testdata/embedded/embed.go
@@ -0,0 +1,12 @@
+package embedded
+
+import (
+	"embed"
+)
+
+//go:embed *
+var files embed.FS
+
+func FS() embed.FS {
+	return files
+}
diff --git a/internal/testdata/embedded/file.txt b/internal/testdata/embedded/file.txt
new file mode 100644
index 00000000..0ba5d842
--- /dev/null
+++ b/internal/testdata/embedded/file.txt
@@ -0,0 +1,1 @@
+This file is embedded.
\ No newline at end of file
diff --git a/internal/testdata/panic.txt b/internal/testdata/panic.txt
new file mode 100644
index 00000000..764ccb2f
--- /dev/null
+++ b/internal/testdata/panic.txt
@@ -0,0 +1,1 @@
+This file must not be accessible from buffalo.FS.
\ No newline at end of file
diff --git a/render/html_test.go b/render/html_test.go
index c31ce75b..f5f269d1 100644
--- a/render/html_test.go
+++ b/render/html_test.go
@@ -68,3 +68,22 @@ func Test_HTML_WithLayout_Override(t *testing.T) {
 	r.NoError(h.Render(bb, Data{"name": "Mark"}))
 	r.Equal("<html>Mark</html>", strings.TrimSpace(bb.String()))
 }
+
+func Test_HTML_LeadingSlash(t *testing.T) {
+	r := require.New(t)
+
+	rootFS := memfs.New()
+	r.NoError(rootFS.WriteFile(htmlTemplate, []byte("<%= name %>"), 0644))
+	r.NoError(rootFS.WriteFile(htmlLayout, []byte("<body><%= yield %></body>"), 0644))
+
+	e := NewEngine()
+	e.TemplatesFS = rootFS
+	e.HTMLLayout = htmlLayout
+
+	h := e.HTML("/my-template.html") // instead of "my-template.html"
+	r.Equal("text/html; charset=utf-8", h.ContentType())
+	bb := &bytes.Buffer{}
+
+	r.NoError(h.Render(bb, Data{"name": "Mark"}))
+	r.Equal("<body>Mark</body>", strings.TrimSpace(bb.String()))
+}