Skip to content
This repository has been archived by the owner on Feb 24, 2024. It is now read-only.

Commit

Permalink
Merge pull request #2180 from fasmat/feature/reload-fs
Browse files Browse the repository at this point in the history
Add reload.FS for hot reloading during development
  • Loading branch information
paganotoni authored Nov 28, 2021
2 parents 691ddff + 5927867 commit 4e1771c
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 0 deletions.
76 changes: 76 additions & 0 deletions fs.go
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
}
100 changes: 100 additions & 0 deletions fs_test.go
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 added internal/testdata/disk/embed.go
Empty file.
1 change: 1 addition & 0 deletions internal/testdata/disk/file.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file is on disk.
Empty file.
12 changes: 12 additions & 0 deletions internal/testdata/embedded/embed.go
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
}
1 change: 1 addition & 0 deletions internal/testdata/embedded/file.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file is embedded.
1 change: 1 addition & 0 deletions internal/testdata/panic.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file must not be accessible from buffalo.FS.

0 comments on commit 4e1771c

Please sign in to comment.