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