205 lines
4.7 KiB
Go
205 lines
4.7 KiB
Go
|
package packr
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"compress/gzip"
|
||
|
"io/ioutil"
|
||
|
"net/http"
|
||
|
"os"
|
||
|
"path"
|
||
|
"path/filepath"
|
||
|
"runtime"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/pkg/errors"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
// ErrResOutsideBox gets returned in case of the requested resources being outside the box
|
||
|
ErrResOutsideBox = errors.New("Can't find a resource outside the box")
|
||
|
)
|
||
|
|
||
|
// NewBox returns a Box that can be used to
|
||
|
// retrieve files from either disk or the embedded
|
||
|
// binary.
|
||
|
func NewBox(path string) Box {
|
||
|
var cd string
|
||
|
if !filepath.IsAbs(path) {
|
||
|
_, filename, _, _ := runtime.Caller(1)
|
||
|
cd = filepath.Dir(filename)
|
||
|
}
|
||
|
|
||
|
// this little hack courtesy of the `-cover` flag!!
|
||
|
cov := filepath.Join("_test", "_obj_test")
|
||
|
cd = strings.Replace(cd, string(filepath.Separator)+cov, "", 1)
|
||
|
if !filepath.IsAbs(cd) && cd != "" {
|
||
|
cd = filepath.Join(GoPath(), "src", cd)
|
||
|
}
|
||
|
|
||
|
return Box{
|
||
|
Path: path,
|
||
|
callingDir: cd,
|
||
|
data: map[string][]byte{},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Box represent a folder on a disk you want to
|
||
|
// have access to in the built Go binary.
|
||
|
type Box struct {
|
||
|
Path string
|
||
|
callingDir string
|
||
|
data map[string][]byte
|
||
|
directories map[string]bool
|
||
|
}
|
||
|
|
||
|
// AddString converts t to a byteslice and delegates to AddBytes to add to b.data
|
||
|
func (b Box) AddString(path string, t string) {
|
||
|
b.AddBytes(path, []byte(t))
|
||
|
}
|
||
|
|
||
|
// AddBytes sets t in b.data by the given path
|
||
|
func (b Box) AddBytes(path string, t []byte) {
|
||
|
b.data[path] = t
|
||
|
}
|
||
|
|
||
|
// String of the file asked for or an empty string.
|
||
|
func (b Box) String(name string) string {
|
||
|
return string(b.Bytes(name))
|
||
|
}
|
||
|
|
||
|
// MustString returns either the string of the requested
|
||
|
// file or an error if it can not be found.
|
||
|
func (b Box) MustString(name string) (string, error) {
|
||
|
bb, err := b.MustBytes(name)
|
||
|
return string(bb), err
|
||
|
}
|
||
|
|
||
|
// Bytes of the file asked for or an empty byte slice.
|
||
|
func (b Box) Bytes(name string) []byte {
|
||
|
bb, _ := b.MustBytes(name)
|
||
|
return bb
|
||
|
}
|
||
|
|
||
|
// MustBytes returns either the byte slice of the requested
|
||
|
// file or an error if it can not be found.
|
||
|
func (b Box) MustBytes(name string) ([]byte, error) {
|
||
|
f, err := b.find(name)
|
||
|
if err == nil {
|
||
|
bb := &bytes.Buffer{}
|
||
|
bb.ReadFrom(f)
|
||
|
return bb.Bytes(), err
|
||
|
}
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// Has returns true if the resource exists in the box
|
||
|
func (b Box) Has(name string) bool {
|
||
|
_, err := b.find(name)
|
||
|
if err != nil {
|
||
|
return false
|
||
|
}
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func (b Box) decompress(bb []byte) []byte {
|
||
|
reader, err := gzip.NewReader(bytes.NewReader(bb))
|
||
|
if err != nil {
|
||
|
return bb
|
||
|
}
|
||
|
data, err := ioutil.ReadAll(reader)
|
||
|
if err != nil {
|
||
|
return bb
|
||
|
}
|
||
|
return data
|
||
|
}
|
||
|
|
||
|
func (b Box) find(name string) (File, error) {
|
||
|
if bb, ok := b.data[name]; ok {
|
||
|
return newVirtualFile(name, bb), nil
|
||
|
}
|
||
|
if b.directories == nil {
|
||
|
b.indexDirectories()
|
||
|
}
|
||
|
|
||
|
cleanName := filepath.ToSlash(filepath.Clean(name))
|
||
|
// Ensure name is not outside the box
|
||
|
if strings.HasPrefix(cleanName, "../") {
|
||
|
return nil, ErrResOutsideBox
|
||
|
}
|
||
|
// Absolute name is considered as relative to the box root
|
||
|
cleanName = strings.TrimPrefix(cleanName, "/")
|
||
|
|
||
|
// Try to get the resource from the box
|
||
|
if _, ok := data[b.Path]; ok {
|
||
|
if bb, ok := data[b.Path][cleanName]; ok {
|
||
|
bb = b.decompress(bb)
|
||
|
return newVirtualFile(cleanName, bb), nil
|
||
|
}
|
||
|
if _, ok := b.directories[cleanName]; ok {
|
||
|
return newVirtualDir(cleanName), nil
|
||
|
}
|
||
|
if filepath.Ext(cleanName) != "" {
|
||
|
// The Handler created by http.FileSystem checks for those errors and
|
||
|
// returns http.StatusNotFound instead of http.StatusInternalServerError.
|
||
|
return nil, os.ErrNotExist
|
||
|
}
|
||
|
return nil, os.ErrNotExist
|
||
|
}
|
||
|
|
||
|
// Not found in the box virtual fs, try to get it from the file system
|
||
|
cleanName = filepath.FromSlash(cleanName)
|
||
|
p := filepath.Join(b.callingDir, b.Path, cleanName)
|
||
|
return fileFor(p, cleanName)
|
||
|
}
|
||
|
|
||
|
// Open returns a File using the http.File interface
|
||
|
func (b Box) Open(name string) (http.File, error) {
|
||
|
return b.find(name)
|
||
|
}
|
||
|
|
||
|
// List shows "What's in the box?"
|
||
|
func (b Box) List() []string {
|
||
|
var keys []string
|
||
|
|
||
|
if b.data == nil || len(b.data) == 0 {
|
||
|
b.Walk(func(path string, info File) error {
|
||
|
finfo, _ := info.FileInfo()
|
||
|
if !finfo.IsDir() {
|
||
|
keys = append(keys, finfo.Name())
|
||
|
}
|
||
|
return nil
|
||
|
})
|
||
|
} else {
|
||
|
for k := range b.data {
|
||
|
keys = append(keys, k)
|
||
|
}
|
||
|
}
|
||
|
return keys
|
||
|
}
|
||
|
|
||
|
func (b *Box) indexDirectories() {
|
||
|
b.directories = map[string]bool{}
|
||
|
if _, ok := data[b.Path]; ok {
|
||
|
for name := range data[b.Path] {
|
||
|
prefix, _ := path.Split(name)
|
||
|
// Even on Windows the suffix appears to be a /
|
||
|
prefix = strings.TrimSuffix(prefix, "/")
|
||
|
b.directories[prefix] = true
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func fileFor(p string, name string) (File, error) {
|
||
|
fi, err := os.Stat(p)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
if fi.IsDir() {
|
||
|
return newVirtualDir(p), nil
|
||
|
}
|
||
|
if bb, err := ioutil.ReadFile(p); err == nil {
|
||
|
return newVirtualFile(name, bb), nil
|
||
|
}
|
||
|
return nil, os.ErrNotExist
|
||
|
}
|