// Package embed implements all file embedding logic for github.com/mjibson/esc. package embed import ( "bytes" "compress/gzip" "encoding/base64" "fmt" "io/ioutil" "os" "path" "path/filepath" "regexp" "sort" "strconv" "strings" "text/template" ) // Config contains all information needed to run esc. type Config struct { // OutputFile is the file name to write output, else stdout. OutputFile string // Package name for the generated file. Package string // Prefix is stripped from filenames. Prefix string // Ignore is the regexp for files we should ignore (for example `\.DS_Store`). Ignore string // Include is the regexp for files to include. If provided, only files that // match will be included. Include string // ModTime is the Unix timestamp to override as modification time for all files. ModTime string // Private, if true, causes autogenerated functions to be unexported. Private bool // NoCompression, if true, stores the files without compression. NoCompression bool // Files is the list of files or directories to embed. Files []string } var modTime *int64 type headerTemplateParams struct { Invocation string PackageName string FunctionPrefix string } type _escFile struct { data []byte local string fileinfo os.FileInfo } // Run executes a Config. func Run(conf *Config) error { var err error if conf.ModTime != "" { i, err := strconv.ParseInt(conf.ModTime, 10, 64) if err != nil { return fmt.Errorf("modtime must be an integer: %v", err) } modTime = &i } var fnames, dirnames []string content := make(map[string]_escFile) prefix := filepath.ToSlash(conf.Prefix) var ignoreRegexp *regexp.Regexp if conf.Ignore != "" { ignoreRegexp, err = regexp.Compile(conf.Ignore) if err != nil { return err } } var includeRegexp *regexp.Regexp if conf.Include != "" { includeRegexp, err = regexp.Compile(conf.Include) if err != nil { return err } } for _, base := range conf.Files { files := []string{base} for len(files) > 0 { fname := files[0] files = files[1:] if ignoreRegexp != nil && ignoreRegexp.MatchString(fname) { continue } f, err := os.Open(fname) if err != nil { return err } fi, err := f.Stat() if err != nil { return err } if fi.IsDir() { fis, err := f.Readdir(0) if err != nil { return err } for _, fi := range fis { files = append(files, filepath.Join(fname, fi.Name())) } } else if includeRegexp == nil || includeRegexp.MatchString(fname) { b, err := ioutil.ReadAll(f) if err != nil { return err } fpath := filepath.ToSlash(fname) n := strings.TrimPrefix(fpath, prefix) n = path.Join("/", n) if _, ok := content[n]; ok { return fmt.Errorf("%s, %s: duplicate name after prefix removal", n, fpath) } content[n] = _escFile{data: b, local: fpath, fileinfo: fi} fnames = append(fnames, n) } f.Close() } } sort.Strings(fnames) w := new(bytes.Buffer) headerText, err := header(conf.Package, !(conf.Private)) if nil != err { return fmt.Errorf("failed to expand autogenerated code: %s", err) } if _, err := w.Write(headerText); err != nil { return fmt.Errorf("failed to write output: %s", err) } dirs := map[string]bool{"/": true} gzipLevel := gzip.BestCompression if conf.NoCompression { gzipLevel = gzip.NoCompression } for _, fname := range fnames { f := content[fname] for b := path.Dir(fname); b != "/"; b = path.Dir(b) { dirs[b] = true } var buf bytes.Buffer gw, err := gzip.NewWriterLevel(&buf, gzipLevel) if err != nil { return err } if _, err := gw.Write(f.data); err != nil { return err } if err := gw.Close(); err != nil { return err } t := f.fileinfo.ModTime().Unix() if modTime != nil { t = *modTime } fmt.Fprintf(w, ` %q: { local: %q, size: %v, modtime: %v, compressed: %s, },%s`, fname, f.local, len(f.data), t, segment(&buf), "\n") } for d := range dirs { dirnames = append(dirnames, d) } sort.Strings(dirnames) for _, dir := range dirnames { local := path.Join(prefix, dir) if len(local) == 0 { local = "." } if local[0] == '/' { // Read dirs relative to the go proc's cwd vs system's // fs root. local = local[1:] } fmt.Fprintf(w, ` %q: { isDir: true, local: %q, },%s`, dir, local, "\n") } w.WriteString(footer) out := os.Stdout if conf.OutputFile != "" { if out, err = os.Create(conf.OutputFile); err != nil { return err } } if _, err := w.WriteTo(out); err != nil { return err } if conf.OutputFile != "" { return out.Close() } return nil } func segment(s *bytes.Buffer) string { var b bytes.Buffer b64 := base64.NewEncoder(base64.StdEncoding, &b) b64.Write(s.Bytes()) b64.Close() res := "`\n" chunk := make([]byte, 80) for n, _ := b.Read(chunk); n > 0; n, _ = b.Read(chunk) { res += string(chunk[0:n]) + "\n" } return res + "`" } func header(packageName string, enableExports bool) ([]byte, error) { functionPrefix := "" if !enableExports { functionPrefix = "_esc" } headerParams := headerTemplateParams{ Invocation: strings.Join(os.Args[1:], " "), PackageName: packageName, FunctionPrefix: functionPrefix, } tmpl, err := template.New("").Parse(headerTemplate) if nil != err { return nil, err } var b bytes.Buffer err = tmpl.Execute(&b, headerParams) if nil != err { return nil, err } return b.Bytes(), nil } const ( headerTemplate = `// Code generated by "esc {{.Invocation}}"; DO NOT EDIT. package {{.PackageName}} import ( "bytes" "compress/gzip" "encoding/base64" "io/ioutil" "net/http" "os" "path" "sync" "time" ) type _escLocalFS struct{} var _escLocal _escLocalFS type _escStaticFS struct{} var _escStatic _escStaticFS type _escDirectory struct { fs http.FileSystem name string } type _escFile struct { compressed string size int64 modtime int64 local string isDir bool once sync.Once data []byte name string } func (_escLocalFS) Open(name string) (http.File, error) { f, present := _escData[path.Clean(name)] if !present { return nil, os.ErrNotExist } return os.Open(f.local) } func (_escStaticFS) prepare(name string) (*_escFile, error) { f, present := _escData[path.Clean(name)] if !present { return nil, os.ErrNotExist } var err error f.once.Do(func() { f.name = path.Base(name) if f.size == 0 { return } var gr *gzip.Reader b64 := base64.NewDecoder(base64.StdEncoding, bytes.NewBufferString(f.compressed)) gr, err = gzip.NewReader(b64) if err != nil { return } f.data, err = ioutil.ReadAll(gr) }) if err != nil { return nil, err } return f, nil } func (fs _escStaticFS) Open(name string) (http.File, error) { f, err := fs.prepare(name) if err != nil { return nil, err } return f.File() } func (dir _escDirectory) Open(name string) (http.File, error) { return dir.fs.Open(dir.name + name) } func (f *_escFile) File() (http.File, error) { type httpFile struct { *bytes.Reader *_escFile } return &httpFile{ Reader: bytes.NewReader(f.data), _escFile: f, }, nil } func (f *_escFile) Close() error { return nil } func (f *_escFile) Readdir(count int) ([]os.FileInfo, error) { return nil, nil } func (f *_escFile) Stat() (os.FileInfo, error) { return f, nil } func (f *_escFile) Name() string { return f.name } func (f *_escFile) Size() int64 { return f.size } func (f *_escFile) Mode() os.FileMode { return 0 } func (f *_escFile) ModTime() time.Time { return time.Unix(f.modtime, 0) } func (f *_escFile) IsDir() bool { return f.isDir } func (f *_escFile) Sys() interface{} { return f } // {{.FunctionPrefix}}FS returns a http.Filesystem for the embedded assets. If useLocal is true, // the filesystem's contents are instead used. func {{.FunctionPrefix}}FS(useLocal bool) http.FileSystem { if useLocal { return _escLocal } return _escStatic } // {{.FunctionPrefix}}Dir returns a http.Filesystem for the embedded assets on a given prefix dir. // If useLocal is true, the filesystem's contents are instead used. func {{.FunctionPrefix}}Dir(useLocal bool, name string) http.FileSystem { if useLocal { return _escDirectory{fs: _escLocal, name: name} } return _escDirectory{fs: _escStatic, name: name} } // {{.FunctionPrefix}}FSByte returns the named file from the embedded assets. If useLocal is // true, the filesystem's contents are instead used. func {{.FunctionPrefix}}FSByte(useLocal bool, name string) ([]byte, error) { if useLocal { f, err := _escLocal.Open(name) if err != nil { return nil, err } b, err := ioutil.ReadAll(f) _ = f.Close() return b, err } f, err := _escStatic.prepare(name) if err != nil { return nil, err } return f.data, nil } // {{.FunctionPrefix}}FSMustByte is the same as {{.FunctionPrefix}}FSByte, but panics if name is not present. func {{.FunctionPrefix}}FSMustByte(useLocal bool, name string) []byte { b, err := {{.FunctionPrefix}}FSByte(useLocal, name) if err != nil { panic(err) } return b } // {{.FunctionPrefix}}FSString is the string version of {{.FunctionPrefix}}FSByte. func {{.FunctionPrefix}}FSString(useLocal bool, name string) (string, error) { b, err := {{.FunctionPrefix}}FSByte(useLocal, name) return string(b), err } // {{.FunctionPrefix}}FSMustString is the string version of {{.FunctionPrefix}}FSMustByte. func {{.FunctionPrefix}}FSMustString(useLocal bool, name string) string { return string({{.FunctionPrefix}}FSMustByte(useLocal, name)) } var _escData = map[string]*_escFile{ ` footer = `} ` )