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