blob: d221101d9e26eec17cb3b9239f8c438e8c5e48f1 [file] [log] [blame]
Serge Bazanskicc25bdf2018-10-25 14:02:58 +02001package 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}