...

Source file src/github.com/shurcooL/vfsgen/generator.go

Documentation: github.com/shurcooL/vfsgen

     1  package vfsgen
     2  
     3  import (
     4  	"bytes"
     5  	"compress/gzip"
     6  	"errors"
     7  	"io"
     8  	"net/http"
     9  	"os"
    10  	pathpkg "path"
    11  	"sort"
    12  	"strconv"
    13  	"text/template"
    14  	"time"
    15  
    16  	"github.com/shurcooL/httpfs/vfsutil"
    17  )
    18  
    19  // Generate Go code that statically implements input filesystem,
    20  // write the output to a file specified in opt.
    21  func Generate(input http.FileSystem, opt Options) error {
    22  	opt.fillMissing()
    23  
    24  	// Use an in-memory buffer to generate the entire output.
    25  	buf := new(bytes.Buffer)
    26  
    27  	err := t.ExecuteTemplate(buf, "Header", opt)
    28  	if err != nil {
    29  		return err
    30  	}
    31  
    32  	var toc toc
    33  	err = findAndWriteFiles(buf, input, &toc)
    34  	if err != nil {
    35  		return err
    36  	}
    37  
    38  	err = t.ExecuteTemplate(buf, "DirEntries", toc.dirs)
    39  	if err != nil {
    40  		return err
    41  	}
    42  
    43  	err = t.ExecuteTemplate(buf, "Trailer", toc)
    44  	if err != nil {
    45  		return err
    46  	}
    47  
    48  	// Write output file (all at once).
    49  	err = os.WriteFile(opt.Filename, buf.Bytes(), 0644)
    50  	return err
    51  }
    52  
    53  type toc struct {
    54  	dirs []*dirInfo
    55  
    56  	HasCompressedFile bool // There's at least one compressedFile.
    57  	HasFile           bool // There's at least one uncompressed file.
    58  }
    59  
    60  // fileInfo is a definition of a file.
    61  type fileInfo struct {
    62  	Path             string
    63  	Name             string
    64  	ModTime          time.Time
    65  	UncompressedSize int64
    66  }
    67  
    68  // dirInfo is a definition of a directory.
    69  type dirInfo struct {
    70  	Path    string
    71  	Name    string
    72  	ModTime time.Time
    73  	Entries []string
    74  }
    75  
    76  // findAndWriteFiles recursively finds all the file paths in the given directory tree.
    77  // They are added to the given map as keys. Values will be safe function names
    78  // for each file, which will be used when generating the output code.
    79  func findAndWriteFiles(buf *bytes.Buffer, fs http.FileSystem, toc *toc) error {
    80  	walkFn := func(path string, fi os.FileInfo, r io.ReadSeeker, err error) error {
    81  		if err != nil {
    82  			// Consider all errors reading the input filesystem as fatal.
    83  			return err
    84  		}
    85  
    86  		switch fi.IsDir() {
    87  		case false:
    88  			file := &fileInfo{
    89  				Path:             path,
    90  				Name:             pathpkg.Base(path),
    91  				ModTime:          fi.ModTime().UTC(),
    92  				UncompressedSize: fi.Size(),
    93  			}
    94  
    95  			marker := buf.Len()
    96  
    97  			// Write CompressedFileInfo.
    98  			err = writeCompressedFileInfo(buf, file, r)
    99  			switch err {
   100  			default:
   101  				return err
   102  			case nil:
   103  				toc.HasCompressedFile = true
   104  			// If compressed file is not smaller than original, revert and write original file.
   105  			case errCompressedNotSmaller:
   106  				_, err = r.Seek(0, io.SeekStart)
   107  				if err != nil {
   108  					return err
   109  				}
   110  
   111  				buf.Truncate(marker)
   112  
   113  				// Write FileInfo.
   114  				err = writeFileInfo(buf, file, r)
   115  				if err != nil {
   116  					return err
   117  				}
   118  				toc.HasFile = true
   119  			}
   120  		case true:
   121  			entries, err := readDirPaths(fs, path)
   122  			if err != nil {
   123  				return err
   124  			}
   125  
   126  			dir := &dirInfo{
   127  				Path:    path,
   128  				Name:    pathpkg.Base(path),
   129  				ModTime: fi.ModTime().UTC(),
   130  				Entries: entries,
   131  			}
   132  
   133  			toc.dirs = append(toc.dirs, dir)
   134  
   135  			// Write DirInfo.
   136  			err = t.ExecuteTemplate(buf, "DirInfo", dir)
   137  			if err != nil {
   138  				return err
   139  			}
   140  		}
   141  
   142  		return nil
   143  	}
   144  
   145  	err := vfsutil.WalkFiles(fs, "/", walkFn)
   146  	return err
   147  }
   148  
   149  // readDirPaths reads the directory named by dirname and returns
   150  // a sorted list of directory paths.
   151  func readDirPaths(fs http.FileSystem, dirname string) ([]string, error) {
   152  	fis, err := vfsutil.ReadDir(fs, dirname)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  	paths := make([]string, len(fis))
   157  	for i := range fis {
   158  		paths[i] = pathpkg.Join(dirname, fis[i].Name())
   159  	}
   160  	sort.Strings(paths)
   161  	return paths, nil
   162  }
   163  
   164  // writeCompressedFileInfo writes CompressedFileInfo.
   165  // It returns errCompressedNotSmaller if compressed file is not smaller than original.
   166  func writeCompressedFileInfo(w io.Writer, file *fileInfo, r io.Reader) error {
   167  	err := t.ExecuteTemplate(w, "CompressedFileInfo-Before", file)
   168  	if err != nil {
   169  		return err
   170  	}
   171  	sw := &stringWriter{Writer: w}
   172  	gw, _ := gzip.NewWriterLevel(sw, gzip.BestCompression)
   173  	_, err = io.Copy(gw, r)
   174  	if err != nil {
   175  		return err
   176  	}
   177  	err = gw.Close()
   178  	if err != nil {
   179  		return err
   180  	}
   181  	if sw.N >= file.UncompressedSize {
   182  		return errCompressedNotSmaller
   183  	}
   184  	err = t.ExecuteTemplate(w, "CompressedFileInfo-After", file)
   185  	return err
   186  }
   187  
   188  var errCompressedNotSmaller = errors.New("compressed file is not smaller than original")
   189  
   190  // Write FileInfo.
   191  func writeFileInfo(w io.Writer, file *fileInfo, r io.Reader) error {
   192  	err := t.ExecuteTemplate(w, "FileInfo-Before", file)
   193  	if err != nil {
   194  		return err
   195  	}
   196  	sw := &stringWriter{Writer: w}
   197  	_, err = io.Copy(sw, r)
   198  	if err != nil {
   199  		return err
   200  	}
   201  	err = t.ExecuteTemplate(w, "FileInfo-After", file)
   202  	return err
   203  }
   204  
   205  var t = template.Must(template.New("").Funcs(template.FuncMap{
   206  	"quote": strconv.Quote,
   207  	"comment": func(s string) (string, error) {
   208  		var buf bytes.Buffer
   209  		cw := &commentWriter{W: &buf}
   210  		_, err := io.WriteString(cw, s)
   211  		if err != nil {
   212  			return "", err
   213  		}
   214  		err = cw.Close()
   215  		return buf.String(), err
   216  	},
   217  }).Parse(`{{define "Header"}}// Code generated by vfsgen; DO NOT EDIT.
   218  
   219  {{with .BuildTags}}//go:build {{.}}
   220  
   221  {{end}}package {{.PackageName}}
   222  
   223  import (
   224  	"bytes"
   225  	"compress/gzip"
   226  	"fmt"
   227  	"io"
   228  	"net/http"
   229  	"os"
   230  	pathpkg "path"
   231  	"time"
   232  )
   233  
   234  {{comment .VariableComment}}
   235  var {{.VariableName}} = func() http.FileSystem {
   236  	fs := vfsgen۰FS{
   237  {{end}}
   238  
   239  
   240  
   241  {{define "CompressedFileInfo-Before"}}		{{quote .Path}}: &vfsgen۰CompressedFileInfo{
   242  			name:             {{quote .Name}},
   243  			modTime:          {{template "Time" .ModTime}},
   244  			uncompressedSize: {{.UncompressedSize}},
   245  {{/* This blank line separating compressedContent is neccessary to prevent potential gofmt issues. See issue #19. */}}
   246  			compressedContent: []byte("{{end}}{{define "CompressedFileInfo-After"}}"),
   247  		},
   248  {{end}}
   249  
   250  
   251  
   252  {{define "FileInfo-Before"}}		{{quote .Path}}: &vfsgen۰FileInfo{
   253  			name:    {{quote .Name}},
   254  			modTime: {{template "Time" .ModTime}},
   255  			content: []byte("{{end}}{{define "FileInfo-After"}}"),
   256  		},
   257  {{end}}
   258  
   259  
   260  
   261  {{define "DirInfo"}}		{{quote .Path}}: &vfsgen۰DirInfo{
   262  			name:    {{quote .Name}},
   263  			modTime: {{template "Time" .ModTime}},
   264  		},
   265  {{end}}
   266  
   267  
   268  
   269  {{define "DirEntries"}}	}
   270  {{range .}}{{if .Entries}}	fs[{{quote .Path}}].(*vfsgen۰DirInfo).entries = []os.FileInfo{{"{"}}{{range .Entries}}
   271  		fs[{{quote .}}].(os.FileInfo),{{end}}
   272  	}
   273  {{end}}{{end}}
   274  	return fs
   275  }()
   276  {{end}}
   277  
   278  
   279  
   280  {{define "Trailer"}}
   281  type vfsgen۰FS map[string]interface{}
   282  
   283  func (fs vfsgen۰FS) Open(path string) (http.File, error) {
   284  	path = pathpkg.Clean("/" + path)
   285  	f, ok := fs[path]
   286  	if !ok {
   287  		return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
   288  	}
   289  
   290  	switch f := f.(type) {{"{"}}{{if .HasCompressedFile}}
   291  	case *vfsgen۰CompressedFileInfo:
   292  		gr, err := gzip.NewReader(bytes.NewReader(f.compressedContent))
   293  		if err != nil {
   294  			// This should never happen because we generate the gzip bytes such that they are always valid.
   295  			panic("unexpected error reading own gzip compressed bytes: " + err.Error())
   296  		}
   297  		return &vfsgen۰CompressedFile{
   298  			vfsgen۰CompressedFileInfo: f,
   299  			gr:                        gr,
   300  		}, nil{{end}}{{if .HasFile}}
   301  	case *vfsgen۰FileInfo:
   302  		return &vfsgen۰File{
   303  			vfsgen۰FileInfo: f,
   304  			Reader:          bytes.NewReader(f.content),
   305  		}, nil{{end}}
   306  	case *vfsgen۰DirInfo:
   307  		return &vfsgen۰Dir{
   308  			vfsgen۰DirInfo: f,
   309  		}, nil
   310  	default:
   311  		// This should never happen because we generate only the above types.
   312  		panic(fmt.Sprintf("unexpected type %T", f))
   313  	}
   314  }
   315  {{if .HasCompressedFile}}
   316  // vfsgen۰CompressedFileInfo is a static definition of a gzip compressed file.
   317  type vfsgen۰CompressedFileInfo struct {
   318  	name              string
   319  	modTime           time.Time
   320  	compressedContent []byte
   321  	uncompressedSize  int64
   322  }
   323  
   324  func (f *vfsgen۰CompressedFileInfo) Readdir(count int) ([]os.FileInfo, error) {
   325  	return nil, fmt.Errorf("cannot Readdir from file %s", f.name)
   326  }
   327  func (f *vfsgen۰CompressedFileInfo) Stat() (os.FileInfo, error) { return f, nil }
   328  
   329  func (f *vfsgen۰CompressedFileInfo) GzipBytes() []byte {
   330  	return f.compressedContent
   331  }
   332  
   333  func (f *vfsgen۰CompressedFileInfo) Name() string       { return f.name }
   334  func (f *vfsgen۰CompressedFileInfo) Size() int64        { return f.uncompressedSize }
   335  func (f *vfsgen۰CompressedFileInfo) Mode() os.FileMode  { return 0444 }
   336  func (f *vfsgen۰CompressedFileInfo) ModTime() time.Time { return f.modTime }
   337  func (f *vfsgen۰CompressedFileInfo) IsDir() bool        { return false }
   338  func (f *vfsgen۰CompressedFileInfo) Sys() interface{}   { return nil }
   339  
   340  // vfsgen۰CompressedFile is an opened compressedFile instance.
   341  type vfsgen۰CompressedFile struct {
   342  	*vfsgen۰CompressedFileInfo
   343  	gr      *gzip.Reader
   344  	grPos   int64 // Actual gr uncompressed position.
   345  	seekPos int64 // Seek uncompressed position.
   346  }
   347  
   348  func (f *vfsgen۰CompressedFile) Read(p []byte) (n int, err error) {
   349  	if f.grPos > f.seekPos {
   350  		// Rewind to beginning.
   351  		err = f.gr.Reset(bytes.NewReader(f.compressedContent))
   352  		if err != nil {
   353  			return 0, err
   354  		}
   355  		f.grPos = 0
   356  	}
   357  	if f.grPos < f.seekPos {
   358  		// Fast-forward.
   359  		_, err = io.CopyN(io.Discard, f.gr, f.seekPos-f.grPos)
   360  		if err != nil {
   361  			return 0, err
   362  		}
   363  		f.grPos = f.seekPos
   364  	}
   365  	n, err = f.gr.Read(p)
   366  	f.grPos += int64(n)
   367  	f.seekPos = f.grPos
   368  	return n, err
   369  }
   370  func (f *vfsgen۰CompressedFile) Seek(offset int64, whence int) (int64, error) {
   371  	switch whence {
   372  	case io.SeekStart:
   373  		f.seekPos = 0 + offset
   374  	case io.SeekCurrent:
   375  		f.seekPos += offset
   376  	case io.SeekEnd:
   377  		f.seekPos = f.uncompressedSize + offset
   378  	default:
   379  		panic(fmt.Errorf("invalid whence value: %v", whence))
   380  	}
   381  	return f.seekPos, nil
   382  }
   383  func (f *vfsgen۰CompressedFile) Close() error {
   384  	return f.gr.Close()
   385  }
   386  {{else}}
   387  // We already imported "compress/gzip" but ended up not using it. Avoid unused import error.
   388  var _ *gzip.Reader
   389  {{end}}{{if .HasFile}}
   390  // vfsgen۰FileInfo is a static definition of an uncompressed file (because it's not worth gzip compressing).
   391  type vfsgen۰FileInfo struct {
   392  	name    string
   393  	modTime time.Time
   394  	content []byte
   395  }
   396  
   397  func (f *vfsgen۰FileInfo) Readdir(count int) ([]os.FileInfo, error) {
   398  	return nil, fmt.Errorf("cannot Readdir from file %s", f.name)
   399  }
   400  func (f *vfsgen۰FileInfo) Stat() (os.FileInfo, error) { return f, nil }
   401  
   402  func (f *vfsgen۰FileInfo) NotWorthGzipCompressing() {}
   403  
   404  func (f *vfsgen۰FileInfo) Name() string       { return f.name }
   405  func (f *vfsgen۰FileInfo) Size() int64        { return int64(len(f.content)) }
   406  func (f *vfsgen۰FileInfo) Mode() os.FileMode  { return 0444 }
   407  func (f *vfsgen۰FileInfo) ModTime() time.Time { return f.modTime }
   408  func (f *vfsgen۰FileInfo) IsDir() bool        { return false }
   409  func (f *vfsgen۰FileInfo) Sys() interface{}   { return nil }
   410  
   411  // vfsgen۰File is an opened file instance.
   412  type vfsgen۰File struct {
   413  	*vfsgen۰FileInfo
   414  	*bytes.Reader
   415  }
   416  
   417  func (f *vfsgen۰File) Close() error {
   418  	return nil
   419  }
   420  {{else if not .HasCompressedFile}}
   421  // We already imported "bytes", but ended up not using it. Avoid unused import error.
   422  var _ = bytes.Reader{}
   423  {{end}}
   424  // vfsgen۰DirInfo is a static definition of a directory.
   425  type vfsgen۰DirInfo struct {
   426  	name    string
   427  	modTime time.Time
   428  	entries []os.FileInfo
   429  }
   430  
   431  func (d *vfsgen۰DirInfo) Read([]byte) (int, error) {
   432  	return 0, fmt.Errorf("cannot Read from directory %s", d.name)
   433  }
   434  func (d *vfsgen۰DirInfo) Close() error               { return nil }
   435  func (d *vfsgen۰DirInfo) Stat() (os.FileInfo, error) { return d, nil }
   436  
   437  func (d *vfsgen۰DirInfo) Name() string       { return d.name }
   438  func (d *vfsgen۰DirInfo) Size() int64        { return 0 }
   439  func (d *vfsgen۰DirInfo) Mode() os.FileMode  { return 0755 | os.ModeDir }
   440  func (d *vfsgen۰DirInfo) ModTime() time.Time { return d.modTime }
   441  func (d *vfsgen۰DirInfo) IsDir() bool        { return true }
   442  func (d *vfsgen۰DirInfo) Sys() interface{}   { return nil }
   443  
   444  // vfsgen۰Dir is an opened dir instance.
   445  type vfsgen۰Dir struct {
   446  	*vfsgen۰DirInfo
   447  	pos int // Position within entries for Seek and Readdir.
   448  }
   449  
   450  func (d *vfsgen۰Dir) Seek(offset int64, whence int) (int64, error) {
   451  	if offset == 0 && whence == io.SeekStart {
   452  		d.pos = 0
   453  		return 0, nil
   454  	}
   455  	return 0, fmt.Errorf("unsupported Seek in directory %s", d.name)
   456  }
   457  
   458  func (d *vfsgen۰Dir) Readdir(count int) ([]os.FileInfo, error) {
   459  	if d.pos >= len(d.entries) && count > 0 {
   460  		return nil, io.EOF
   461  	}
   462  	if count <= 0 || count > len(d.entries)-d.pos {
   463  		count = len(d.entries) - d.pos
   464  	}
   465  	e := d.entries[d.pos : d.pos+count]
   466  	d.pos += count
   467  	return e, nil
   468  }
   469  {{end}}
   470  
   471  
   472  
   473  {{define "Time"}}
   474  {{- if .IsZero -}}
   475  	time.Time{}
   476  {{- else -}}
   477  	time.Date({{.Year}}, {{printf "%d" .Month}}, {{.Day}}, {{.Hour}}, {{.Minute}}, {{.Second}}, {{.Nanosecond}}, time.UTC)
   478  {{- end -}}
   479  {{end}}
   480  `))
   481  

View as plain text