1package packr
2
3import (
4	"bytes"
5	"compress/gzip"
6	"io/ioutil"
7	"net/http"
8	"os"
9	"path"
10	"path/filepath"
11	"runtime"
12	"strings"
13
14	"github.com/pkg/errors"
15)
16
17var (
18	// ErrResOutsideBox gets returned in case of the requested resources being outside the box
19	ErrResOutsideBox = errors.New("Can't find a resource outside the box")
20)
21
22// NewBox returns a Box that can be used to
23// retrieve files from either disk or the embedded
24// binary.
25func NewBox(path string) Box {
26	var cd string
27	if !filepath.IsAbs(path) {
28		_, filename, _, _ := runtime.Caller(1)
29		cd = filepath.Dir(filename)
30	}
31
32	// this little hack courtesy of the `-cover` flag!!
33	cov := filepath.Join("_test", "_obj_test")
34	cd = strings.Replace(cd, string(filepath.Separator)+cov, "", 1)
35	if !filepath.IsAbs(cd) && cd != "" {
36		cd = filepath.Join(GoPath(), "src", cd)
37	}
38
39	return Box{
40		Path:       path,
41		callingDir: cd,
42		data:       map[string][]byte{},
43	}
44}
45
46// Box represent a folder on a disk you want to
47// have access to in the built Go binary.
48type Box struct {
49	Path        string
50	callingDir  string
51	data        map[string][]byte
52	directories map[string]bool
53}
54
55// AddString converts t to a byteslice and delegates to AddBytes to add to b.data
56func (b Box) AddString(path string, t string) {
57	b.AddBytes(path, []byte(t))
58}
59
60// AddBytes sets t in b.data by the given path
61func (b Box) AddBytes(path string, t []byte) {
62	b.data[path] = t
63}
64
65// String of the file asked for or an empty string.
66func (b Box) String(name string) string {
67	return string(b.Bytes(name))
68}
69
70// MustString returns either the string of the requested
71// file or an error if it can not be found.
72func (b Box) MustString(name string) (string, error) {
73	bb, err := b.MustBytes(name)
74	return string(bb), err
75}
76
77// Bytes of the file asked for or an empty byte slice.
78func (b Box) Bytes(name string) []byte {
79	bb, _ := b.MustBytes(name)
80	return bb
81}
82
83// MustBytes returns either the byte slice of the requested
84// file or an error if it can not be found.
85func (b Box) MustBytes(name string) ([]byte, error) {
86	f, err := b.find(name)
87	if err == nil {
88		bb := &bytes.Buffer{}
89		bb.ReadFrom(f)
90		return bb.Bytes(), err
91	}
92	return nil, err
93}
94
95// Has returns true if the resource exists in the box
96func (b Box) Has(name string) bool {
97	_, err := b.find(name)
98	if err != nil {
99		return false
100	}
101	return true
102}
103
104func (b Box) decompress(bb []byte) []byte {
105	reader, err := gzip.NewReader(bytes.NewReader(bb))
106	if err != nil {
107		return bb
108	}
109	data, err := ioutil.ReadAll(reader)
110	if err != nil {
111		return bb
112	}
113	return data
114}
115
116func (b Box) find(name string) (File, error) {
117	if bb, ok := b.data[name]; ok {
118		return newVirtualFile(name, bb), nil
119	}
120	if b.directories == nil {
121		b.indexDirectories()
122	}
123
124	cleanName := filepath.ToSlash(filepath.Clean(name))
125	// Ensure name is not outside the box
126	if strings.HasPrefix(cleanName, "../") {
127		return nil, ErrResOutsideBox
128	}
129	// Absolute name is considered as relative to the box root
130	cleanName = strings.TrimPrefix(cleanName, "/")
131
132	// Try to get the resource from the box
133	if _, ok := data[b.Path]; ok {
134		if bb, ok := data[b.Path][cleanName]; ok {
135			bb = b.decompress(bb)
136			return newVirtualFile(cleanName, bb), nil
137		}
138		if _, ok := b.directories[cleanName]; ok {
139			return newVirtualDir(cleanName), nil
140		}
141		if filepath.Ext(cleanName) != "" {
142			// The Handler created by http.FileSystem checks for those errors and
143			// returns http.StatusNotFound instead of http.StatusInternalServerError.
144			return nil, os.ErrNotExist
145		}
146		return nil, os.ErrNotExist
147	}
148
149	// Not found in the box virtual fs, try to get it from the file system
150	cleanName = filepath.FromSlash(cleanName)
151	p := filepath.Join(b.callingDir, b.Path, cleanName)
152	return fileFor(p, cleanName)
153}
154
155// Open returns a File using the http.File interface
156func (b Box) Open(name string) (http.File, error) {
157	return b.find(name)
158}
159
160// List shows "What's in the box?"
161func (b Box) List() []string {
162	var keys []string
163
164	if b.data == nil || len(b.data) == 0 {
165		b.Walk(func(path string, info File) error {
166			finfo, _ := info.FileInfo()
167			if !finfo.IsDir() {
168				keys = append(keys, finfo.Name())
169			}
170			return nil
171		})
172	} else {
173		for k := range b.data {
174			keys = append(keys, k)
175		}
176	}
177	return keys
178}
179
180func (b *Box) indexDirectories() {
181	b.directories = map[string]bool{}
182	if _, ok := data[b.Path]; ok {
183		for name := range data[b.Path] {
184			prefix, _ := path.Split(name)
185			// Even on Windows the suffix appears to be a /
186			prefix = strings.TrimSuffix(prefix, "/")
187			b.directories[prefix] = true
188		}
189	}
190}
191
192func fileFor(p string, name string) (File, error) {
193	fi, err := os.Stat(p)
194	if err != nil {
195		return nil, err
196	}
197	if fi.IsDir() {
198		return newVirtualDir(p), nil
199	}
200	if bb, err := ioutil.ReadFile(p); err == nil {
201		return newVirtualFile(name, bb), nil
202	}
203	return nil, os.ErrNotExist
204}
205