This repository has been archived by the owner on Feb 24, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 579
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2180 from fasmat/feature/reload-fs
Add reload.FS for hot reloading during development
- Loading branch information
Showing
8 changed files
with
191 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
This file is on disk. |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package embedded | ||
|
||
import ( | ||
"embed" | ||
) | ||
|
||
//go:embed * | ||
var files embed.FS | ||
|
||
func FS() embed.FS { | ||
return files | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
This file is embedded. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
This file must not be accessible from buffalo.FS. |