提交 3581377e 编写于 作者: S Simon Pasquier 提交者: Tobias Schmidt

Replace go-bindata with vfsgen (#4430)

Looking at https://tech.townsourced.com/post/embedding-static-files-in-go/ (which was mentioned in the issue), vfsgen has all the needed features.

In particular:

- Reproducible builds (no issue with timestamping).
- Well maintained and relatively popular.
- Integration with go generate.
- Self-contained (no external dependency).

* [WIP] Replace go-bindata by vfsgen
Signed-off-by: NSimon Pasquier <spasquie@redhat.com>

* Add license + remove doc.go
Signed-off-by: NSimon Pasquier <spasquie@redhat.com>

* Generate templates assets
Signed-off-by: NSimon Pasquier <spasquie@redhat.com>

* Use new templates assets
Signed-off-by: NSimon Pasquier <spasquie@redhat.com>

* split static assets
Signed-off-by: NSimon Pasquier <spasquie@redhat.com>

* Idempotent make assets
Signed-off-by: NSimon Pasquier <spasquie@redhat.com>

* Update vendor/
Signed-off-by: NSimon Pasquier <spasquie@redhat.com>

* vendor vfsgendev
Signed-off-by: NSimon Pasquier <spasquie@redhat.com>

* Update README.md
Signed-off-by: NSimon Pasquier <spasquie@redhat.com>

* Simplify assets generation
Signed-off-by: NSimon Pasquier <spasquie@redhat.com>

* Fix README.md
Signed-off-by: NSimon Pasquier <spasquie@redhat.com>

* Use generate helper program instead of vfsgen

This avoids installing vfsgendev in the target environment.
Signed-off-by: NSimon Pasquier <spasquie@redhat.com>

* Remove unused vfsgen package
Signed-off-by: NSimon Pasquier <spasquie@redhat.com>

* Fix Makefile
Signed-off-by: NSimon Pasquier <spasquie@redhat.com>

* vendoring shurcooL/vfsgen
Signed-off-by: NSimon Pasquier <spasquie@redhat.com>

* Fix go generate command
Signed-off-by: NSimon Pasquier <spasquie@redhat.com>

* Sync web/ui/assets_vfsdata.go
Signed-off-by: NSimon Pasquier <spasquie@redhat.com>
上级 ecf676cf
......@@ -23,13 +23,7 @@ STATICCHECK_IGNORE = \
DOCKER_IMAGE_NAME ?= prometheus
ifdef DEBUG
bindata_flags = -debug
endif
.PHONY: assets
assets:
@echo ">> writing assets"
@$(GO) get -u github.com/jteeuwen/go-bindata/...
@go-bindata $(bindata_flags) -pkg ui -o web/ui/bindata.go -ignore '(.*\.map|bootstrap\.js|bootstrap-theme\.css|bootstrap\.css)' web/ui/templates/... web/ui/static/...
@$(GO) fmt ./web/ui
cd $(PREFIX)/web/ui && go generate
// Package filter offers an http.FileSystem wrapper with the ability to keep or skip files.
package filter
import (
"fmt"
"io"
"net/http"
"os"
pathpkg "path"
"time"
)
// Func is a selection function which is provided two arguments,
// its '/'-separated cleaned rooted absolute path (i.e., it always begins with "/"),
// and the os.FileInfo of the considered file.
//
// The path is cleaned via pathpkg.Clean("/" + path).
//
// For example, if the considered file is named "a" and it's inside a directory "dir",
// then the value of path will be "/dir/a".
type Func func(path string, fi os.FileInfo) bool
// Keep returns a filesystem that contains only those entries in source for which
// keep returns true.
func Keep(source http.FileSystem, keep Func) http.FileSystem {
return &filterFS{source: source, keep: keep}
}
// Skip returns a filesystem that contains everything in source, except entries
// for which skip returns true.
func Skip(source http.FileSystem, skip Func) http.FileSystem {
keep := func(path string, fi os.FileInfo) bool {
return !skip(path, fi)
}
return &filterFS{source: source, keep: keep}
}
type filterFS struct {
source http.FileSystem
keep Func // Keep entries that keep returns true for.
}
func (fs *filterFS) Open(path string) (http.File, error) {
f, err := fs.source.Open(path)
if err != nil {
return nil, err
}
fi, err := f.Stat()
if err != nil {
f.Close()
return nil, err
}
if !fs.keep(clean(path), fi) {
// Skip.
f.Close()
return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
}
if !fi.IsDir() {
return f, nil
}
defer f.Close()
fis, err := f.Readdir(0)
if err != nil {
return nil, err
}
var entries []os.FileInfo
for _, fi := range fis {
if !fs.keep(clean(pathpkg.Join(path, fi.Name())), fi) {
// Skip.
continue
}
entries = append(entries, fi)
}
return &dir{
name: fi.Name(),
entries: entries,
modTime: fi.ModTime(),
}, nil
}
// clean turns a potentially relative path into an absolute one.
//
// This is needed to normalize path parameter for selection function.
func clean(path string) string {
return pathpkg.Clean("/" + path)
}
// dir is an opened dir instance.
type dir struct {
name string
modTime time.Time
entries []os.FileInfo
pos int // Position within entries for Seek and Readdir.
}
func (d *dir) Read([]byte) (int, error) {
return 0, fmt.Errorf("cannot Read from directory %s", d.name)
}
func (d *dir) Close() error { return nil }
func (d *dir) Stat() (os.FileInfo, error) { return d, nil }
func (d *dir) Name() string { return d.name }
func (d *dir) Size() int64 { return 0 }
func (d *dir) Mode() os.FileMode { return 0755 | os.ModeDir }
func (d *dir) ModTime() time.Time { return d.modTime }
func (d *dir) IsDir() bool { return true }
func (d *dir) Sys() interface{} { return nil }
func (d *dir) Seek(offset int64, whence int) (int64, error) {
if offset == 0 && whence == io.SeekStart {
d.pos = 0
return 0, nil
}
return 0, fmt.Errorf("unsupported Seek in directory %s", d.name)
}
func (d *dir) Readdir(count int) ([]os.FileInfo, error) {
if d.pos >= len(d.entries) && count > 0 {
return nil, io.EOF
}
if count <= 0 || count > len(d.entries)-d.pos {
count = len(d.entries) - d.pos
}
e := d.entries[d.pos : d.pos+count]
d.pos += count
return e, nil
}
package filter
import (
"os"
pathpkg "path"
)
// FilesWithExtensions returns a filter func that selects files (but not directories)
// that have any of the given extensions. For example:
//
// filter.FilesWithExtensions(".go", ".html")
//
// Would select both .go and .html files. It would not select any directories.
func FilesWithExtensions(exts ...string) Func {
return func(path string, fi os.FileInfo) bool {
if fi.IsDir() {
return false
}
for _, ext := range exts {
if pathpkg.Ext(path) == ext {
return true
}
}
return false
}
}
// Package union offers a simple http.FileSystem that can unify multiple filesystems at various mount points.
package union
import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
// New creates an union filesystem with the provided mapping of mount points to filesystems.
//
// Each mount point must be of form "/mydir". It must start with a '/', and contain a single directory name.
func New(mapping map[string]http.FileSystem) http.FileSystem {
u := &unionFS{
ns: make(map[string]http.FileSystem),
root: &dirInfo{
name: "/",
},
}
for mountPoint, fs := range mapping {
u.bind(mountPoint, fs)
}
return u
}
type unionFS struct {
ns map[string]http.FileSystem // Key is mount point, e.g., "/mydir".
root *dirInfo
}
// bind mounts fs at mountPoint.
// mountPoint must be of form "/mydir". It must start with a '/', and contain a single directory name.
func (u *unionFS) bind(mountPoint string, fs http.FileSystem) {
u.ns[mountPoint] = fs
u.root.entries = append(u.root.entries, &dirInfo{
name: mountPoint[1:],
})
}
// Open opens the named file.
func (u *unionFS) Open(path string) (http.File, error) {
// TODO: Maybe clean path?
if path == "/" {
return &dir{
dirInfo: u.root,
}, nil
}
for prefix, fs := range u.ns {
if path == prefix || strings.HasPrefix(path, prefix+"/") {
innerPath := path[len(prefix):]
if innerPath == "" {
innerPath = "/"
}
return fs.Open(innerPath)
}
}
return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
}
// dirInfo is a static definition of a directory.
type dirInfo struct {
name string
entries []os.FileInfo
}
func (d *dirInfo) Read([]byte) (int, error) {
return 0, fmt.Errorf("cannot Read from directory %s", d.name)
}
func (d *dirInfo) Close() error { return nil }
func (d *dirInfo) Stat() (os.FileInfo, error) { return d, nil }
func (d *dirInfo) Name() string { return d.name }
func (d *dirInfo) Size() int64 { return 0 }
func (d *dirInfo) Mode() os.FileMode { return 0755 | os.ModeDir }
func (d *dirInfo) ModTime() time.Time { return time.Time{} } // Actual mod time is not computed because it's expensive and rarely needed.
func (d *dirInfo) IsDir() bool { return true }
func (d *dirInfo) Sys() interface{} { return nil }
// dir is an opened dir instance.
type dir struct {
*dirInfo
pos int // Position within entries for Seek and Readdir.
}
func (d *dir) Seek(offset int64, whence int) (int64, error) {
if offset == 0 && whence == io.SeekStart {
d.pos = 0
return 0, nil
}
return 0, fmt.Errorf("unsupported Seek in directory %s", d.dirInfo.name)
}
func (d *dir) Readdir(count int) ([]os.FileInfo, error) {
if d.pos >= len(d.dirInfo.entries) && count > 0 {
return nil, io.EOF
}
if count <= 0 || count > len(d.dirInfo.entries)-d.pos {
count = len(d.dirInfo.entries) - d.pos
}
e := d.dirInfo.entries[d.pos : d.pos+count]
d.pos += count
return e, nil
}
package vfsutil
import (
"net/http"
"os"
)
// File implements http.FileSystem using the native file system restricted to a
// specific file served at root.
//
// While the FileSystem.Open method takes '/'-separated paths, a File's string
// value is a filename on the native file system, not a URL, so it is separated
// by filepath.Separator, which isn't necessarily '/'.
type File string
func (f File) Open(name string) (http.File, error) {
if name != "/" {
return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist}
}
return os.Open(string(f))
}
// Package vfsutil implements some I/O utility functions for http.FileSystem.
package vfsutil
import (
"io/ioutil"
"net/http"
"os"
)
// ReadDir reads the contents of the directory associated with file and
// returns a slice of FileInfo values in directory order.
func ReadDir(fs http.FileSystem, name string) ([]os.FileInfo, error) {
f, err := fs.Open(name)
if err != nil {
return nil, err
}
defer f.Close()
return f.Readdir(0)
}
// Stat returns the FileInfo structure describing file.
func Stat(fs http.FileSystem, name string) (os.FileInfo, error) {
f, err := fs.Open(name)
if err != nil {
return nil, err
}
defer f.Close()
return f.Stat()
}
// ReadFile reads the file named by path from fs and returns the contents.
func ReadFile(fs http.FileSystem, path string) ([]byte, error) {
rc, err := fs.Open(path)
if err != nil {
return nil, err
}
defer rc.Close()
return ioutil.ReadAll(rc)
}
package vfsutil
import (
"io"
"net/http"
"os"
pathpkg "path"
"path/filepath"
"sort"
)
// Walk walks the filesystem rooted at root, calling walkFn for each file or
// directory in the filesystem, including root. All errors that arise visiting files
// and directories are filtered by walkFn. The files are walked in lexical
// order.
func Walk(fs http.FileSystem, root string, walkFn filepath.WalkFunc) error {
info, err := Stat(fs, root)
if err != nil {
return walkFn(root, nil, err)
}
return walk(fs, root, info, walkFn)
}
// readDirNames reads the directory named by dirname and returns
// a sorted list of directory entries.
func readDirNames(fs http.FileSystem, dirname string) ([]string, error) {
fis, err := ReadDir(fs, dirname)
if err != nil {
return nil, err
}
names := make([]string, len(fis))
for i := range fis {
names[i] = fis[i].Name()
}
sort.Strings(names)
return names, nil
}
// walk recursively descends path, calling walkFn.
func walk(fs http.FileSystem, path string, info os.FileInfo, walkFn filepath.WalkFunc) error {
err := walkFn(path, info, nil)
if err != nil {
if info.IsDir() && err == filepath.SkipDir {
return nil
}
return err
}
if !info.IsDir() {
return nil
}
names, err := readDirNames(fs, path)
if err != nil {
return walkFn(path, info, err)
}
for _, name := range names {
filename := pathpkg.Join(path, name)
fileInfo, err := Stat(fs, filename)
if err != nil {
if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir {
return err
}
} else {
err = walk(fs, filename, fileInfo, walkFn)
if err != nil {
if !fileInfo.IsDir() || err != filepath.SkipDir {
return err
}
}
}
}
return nil
}
// WalkFilesFunc is the type of the function called for each file or directory visited by WalkFiles.
// It's like filepath.WalkFunc, except it provides an additional ReadSeeker parameter for file being visited.
type WalkFilesFunc func(path string, info os.FileInfo, rs io.ReadSeeker, err error) error
// WalkFiles walks the filesystem rooted at root, calling walkFn for each file or
// directory in the filesystem, including root. In addition to FileInfo, it passes an
// ReadSeeker to walkFn for each file it visits.
func WalkFiles(fs http.FileSystem, root string, walkFn WalkFilesFunc) error {
file, info, err := openStat(fs, root)
if err != nil {
return walkFn(root, nil, nil, err)
}
return walkFiles(fs, root, info, file, walkFn)
}
// walkFiles recursively descends path, calling walkFn.
// It closes the input file after it's done with it, so the caller shouldn't.
func walkFiles(fs http.FileSystem, path string, info os.FileInfo, file http.File, walkFn WalkFilesFunc) error {
err := walkFn(path, info, file, nil)
file.Close()
if err != nil {
if info.IsDir() && err == filepath.SkipDir {
return nil
}
return err
}
if !info.IsDir() {
return nil
}
names, err := readDirNames(fs, path)
if err != nil {
return walkFn(path, info, nil, err)
}
for _, name := range names {
filename := pathpkg.Join(path, name)
file, fileInfo, err := openStat(fs, filename)
if err != nil {
if err := walkFn(filename, nil, nil, err); err != nil && err != filepath.SkipDir {
return err
}
} else {
err = walkFiles(fs, filename, fileInfo, file, walkFn)
// file is closed by walkFiles, so we don't need to close it here.
if err != nil {
if !fileInfo.IsDir() || err != filepath.SkipDir {
return err
}
}
}
}
return nil
}
// openStat performs Open and Stat and returns results, or first error encountered.
// The caller is responsible for closing the returned file when done.
func openStat(fs http.FileSystem, name string) (http.File, os.FileInfo, error) {
f, err := fs.Open(name)
if err != nil {
return nil, nil, err
}
fi, err := f.Stat()
if err != nil {
f.Close()
return nil, nil, err
}
return f, fi, nil
}
Contributing
============
vfsgen is open source, thanks for considering contributing!
Please note that vfsgen aims to be simple and minimalistic, with as little to configure as possible. If you'd like to remove or simplify code (while having tests continue to pass), fix bugs, or improve code (e.g., add missing error checking, etc.), PRs and issues are welcome.
However, if you'd like to add new functionality that increases complexity or scope, please make an issue and discuss your proposal first. I'm unlikely to accept such changes outright. It might be that your request is already a part of other similar packages, or it might fit in their scope better. See [Comparison and Alternatives](https://github.com/shurcooL/vfsgen/tree/README-alternatives-and-comparison-section#comparison) sections.
Thank you!
vfsgen
======
[![Build Status](https://travis-ci.org/shurcooL/vfsgen.svg?branch=master)](https://travis-ci.org/shurcooL/vfsgen) [![GoDoc](https://godoc.org/github.com/shurcooL/vfsgen?status.svg)](https://godoc.org/github.com/shurcooL/vfsgen)
Package vfsgen takes an http.FileSystem (likely at `go generate` time) and
generates Go code that statically implements the provided http.FileSystem.
Features:
- Efficient generated code without unneccessary overhead.
- Uses gzip compression internally (selectively, only for files that compress well).
- Enables direct access to internal gzip compressed bytes via an optional interface.
- Outputs `gofmt`ed Go code.
Installation
------------
```bash
go get -u github.com/shurcooL/vfsgen
```
Usage
-----
This code will generate an assets_vfsdata.go file with `var assets http.FileSystem = ...` that statically implements the contents of "assets" directory.
```Go
var fs http.FileSystem = http.Dir("assets")
err := vfsgen.Generate(fs, vfsgen.Options{})
if err != nil {
log.Fatalln(err)
}
```
Then, in your program, you can use `assets` as any other [`http.FileSystem`](https://godoc.org/net/http#FileSystem), for example:
```Go
file, err := assets.Open("/some/file.txt")
if err != nil { ... }
defer file.Close()
```
```Go
http.Handle("/assets/", http.FileServer(assets))
```
### `go generate` Usage
vfsgen is great to use with go generate directives. The code invoking `vfsgen.Generate` can go in an assets_generate.go file, which can then be invoked via "//go:generate go run assets_generate.go". The input virtual filesystem can read directly from disk, or it can be more involved.
By using build tags, you can create a development mode where assets are loaded directly from disk via `http.Dir`, but then statically implemented for final releases.
For example, suppose your source filesystem is defined in a package with import path "example.com/project/data" as:
```Go
// +build dev
package data
import "net/http"
// Assets contains project assets.
var Assets http.FileSystem = http.Dir("assets")
```
When built with the "dev" build tag, accessing `data.Assets` will read from disk directly via `http.Dir`.
A generate helper file assets_generate.go can be invoked via "//go:generate go run -tags=dev assets_generate.go" directive:
```Go
// +build ignore
package main
import (
"log"
"example.com/project/data"
"github.com/shurcooL/vfsgen"
)
func main() {
err := vfsgen.Generate(data.Assets, vfsgen.Options{
PackageName: "data",
BuildTags: "!dev",
VariableName: "Assets",
})
if err != nil {
log.Fatalln(err)
}
}
```
Note that "dev" build tag is used to access the source filesystem, and the output file will contain "!dev" build tag. That way, the statically implemented version will be used during normal builds and `go get`, when custom builds tags are not specified.
### `vfsgendev` Usage
`vfsgendev` is a binary that can be used to replace the need for the assets_generate.go file.
Make sure it's installed and available in your PATH.
```bash
go get -u github.com/shurcooL/vfsgen/cmd/vfsgendev
```
Then the "//go:generate go run -tags=dev assets_generate.go" directive can be replaced with:
```
//go:generate vfsgendev -source="example.com/project/data".Assets
```
vfsgendev accesses the source variable using "dev" build tag, and generates an output file with "!dev" build tag.
### Additional Embedded Information
All compressed files implement [`httpgzip.GzipByter` interface](https://godoc.org/github.com/shurcooL/httpgzip#GzipByter) for efficient direct access to the internal compressed bytes:
```Go
// GzipByter is implemented by compressed files for
// efficient direct access to the internal compressed bytes.
type GzipByter interface {
// GzipBytes returns gzip compressed contents of the file.
GzipBytes() []byte
}
```
Files that have been determined to not be worth gzip compressing (their compressed size is larger than original) implement [`httpgzip.NotWorthGzipCompressing` interface](https://godoc.org/github.com/shurcooL/httpgzip#NotWorthGzipCompressing):
```Go
// NotWorthGzipCompressing is implemented by files that were determined
// not to be worth gzip compressing (the file size did not decrease as a result).
type NotWorthGzipCompressing interface {
// NotWorthGzipCompressing is a noop. It's implemented in order to indicate
// the file is not worth gzip compressing.
NotWorthGzipCompressing()
}
```
Comparison
----------
vfsgen aims to be conceptually simple to use. The [`http.FileSystem`](https://godoc.org/net/http#FileSystem) abstraction is central to vfsgen. It's used as both input for code generation, and as output in the generated code.
That enables great flexibility through orthogonality, since helpers and wrappers can operate on `http.FileSystem` without knowing about vfsgen. If you want, you can perform pre-processing, minifying assets, merging folders, filtering out files and otherwise modifying input via generic `http.FileSystem` middleware.
It avoids unneccessary overhead by merging what was previously done with two distinct packages into a single package.
It strives to be the best in its class in terms of code quality and efficiency of generated code. However, if your use goals are different, there are other similar packages that may fit your needs better.
### Alternatives
- [`go-bindata`](https://github.com/jteeuwen/go-bindata) - Reads from disk, generates Go code that provides access to data via a [custom API](https://github.com/jteeuwen/go-bindata#accessing-an-asset).
- [`go-bindata-assetfs`](https://github.com/elazarl/go-bindata-assetfs) - Takes output of go-bindata and provides a wrapper that implements `http.FileSystem` interface (the same as what vfsgen outputs directly).
- [`becky`](https://github.com/tv42/becky) - Embeds assets as string literals in Go source.
- [`statik`](https://github.com/rakyll/statik) - Embeds a directory of static files to be accessed via `http.FileSystem` interface (sounds very similar to vfsgen); implementation sourced from [camlistore](https://camlistore.org).
- [`go.rice`](https://github.com/GeertJohan/go.rice) - Makes working with resources such as HTML, JS, CSS, images and templates very easy.
- [`esc`](https://github.com/mjibson/esc) - Embeds files into Go programs and provides `http.FileSystem` interfaces to them.
- [`staticfiles`](https://github.com/bouk/staticfiles) - Allows you to embed a directory of files into your Go binary.
- [`togo`](https://github.com/flazz/togo) - Generates a Go source file with a `[]byte` var containing the given file's contents.
- [`fileb0x`](https://github.com/UnnoTed/fileb0x) - Simple customizable tool to embed files in Go.
- [`embedfiles`](https://github.com/leighmcculloch/embedfiles) - Simple tool for embedding files in Go code as a map.
- [`packr`](https://github.com/gobuffalo/packr) - Simple solution for bundling static assets inside of Go binaries.
- [`rsrc`](https://github.com/akavel/rsrc) - Tool for embedding .ico & manifest resources in Go programs for Windows.
Attribution
-----------
This package was originally based on the excellent work by [@jteeuwen](https://github.com/jteeuwen) on [`go-bindata`](https://github.com/jteeuwen/go-bindata) and [@elazarl](https://github.com/elazarl) on [`go-bindata-assetfs`](https://github.com/elazarl/go-bindata-assetfs).
License
-------
- [MIT License](https://opensource.org/licenses/mit-license.php)
package vfsgen
import "io"
// commentWriter writes a Go comment to the underlying io.Writer,
// using line comment form (//).
type commentWriter struct {
W io.Writer
wroteSlashes bool // Wrote "//" at the beginning of the current line.
}
func (c *commentWriter) Write(p []byte) (int, error) {
var n int
for i, b := range p {
if !c.wroteSlashes {
s := "//"
if b != '\n' {
s = "// "
}
if _, err := io.WriteString(c.W, s); err != nil {
return n, err
}
c.wroteSlashes = true
}
n0, err := c.W.Write(p[i : i+1])
n += n0
if err != nil {
return n, err
}
if b == '\n' {
c.wroteSlashes = false
}
}
return len(p), nil
}
func (c *commentWriter) Close() error {
if !c.wroteSlashes {
if _, err := io.WriteString(c.W, "//"); err != nil {
return err
}
c.wroteSlashes = true
}
return nil
}
/*
Package vfsgen takes an http.FileSystem (likely at `go generate` time) and
generates Go code that statically implements the provided http.FileSystem.
Features:
- Efficient generated code without unneccessary overhead.
- Uses gzip compression internally (selectively, only for files that compress well).
- Enables direct access to internal gzip compressed bytes via an optional interface.
- Outputs `gofmt`ed Go code.
*/
package vfsgen
package vfsgen
import (
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
pathpkg "path"
"sort"
"strconv"
"text/template"
"time"
"github.com/shurcooL/httpfs/vfsutil"
)
// Generate Go code that statically implements input filesystem,
// write the output to a file specified in opt.
func Generate(input http.FileSystem, opt Options) error {
opt.fillMissing()
// Use an in-memory buffer to generate the entire output.
buf := new(bytes.Buffer)
err := t.ExecuteTemplate(buf, "Header", opt)
if err != nil {
return err
}
var toc toc
err = findAndWriteFiles(buf, input, &toc)
if err != nil {
return err
}
err = t.ExecuteTemplate(buf, "DirEntries", toc.dirs)
if err != nil {
return err
}
err = t.ExecuteTemplate(buf, "Trailer", toc)
if err != nil {
return err
}
// Write output file (all at once).
fmt.Println("writing", opt.Filename)
err = ioutil.WriteFile(opt.Filename, buf.Bytes(), 0644)
return err
}
type toc struct {
dirs []*dirInfo
HasCompressedFile bool // There's at least one compressedFile.
HasFile bool // There's at least one uncompressed file.
}
// fileInfo is a definition of a file.
type fileInfo struct {
Path string
Name string
ModTime time.Time
UncompressedSize int64
}
// dirInfo is a definition of a directory.
type dirInfo struct {
Path string
Name string
ModTime time.Time
Entries []string
}
// findAndWriteFiles recursively finds all the file paths in the given directory tree.
// They are added to the given map as keys. Values will be safe function names
// for each file, which will be used when generating the output code.
func findAndWriteFiles(buf *bytes.Buffer, fs http.FileSystem, toc *toc) error {
walkFn := func(path string, fi os.FileInfo, r io.ReadSeeker, err error) error {
if err != nil {
log.Printf("can't stat file %q: %v\n", path, err)
return nil
}
switch fi.IsDir() {
case false:
file := &fileInfo{
Path: path,
Name: pathpkg.Base(path),
ModTime: fi.ModTime().UTC(),
UncompressedSize: fi.Size(),
}
marker := buf.Len()
// Write CompressedFileInfo.
err = writeCompressedFileInfo(buf, file, r)
switch err {
default:
return err
case nil:
toc.HasCompressedFile = true
// If compressed file is not smaller than original, revert and write original file.
case errCompressedNotSmaller:
_, err = r.Seek(0, io.SeekStart)
if err != nil {
return err
}
buf.Truncate(marker)
// Write FileInfo.
err = writeFileInfo(buf, file, r)
if err != nil {
return err
}
toc.HasFile = true
}
case true:
entries, err := readDirPaths(fs, path)
if err != nil {
return err
}
dir := &dirInfo{
Path: path,
Name: pathpkg.Base(path),
ModTime: fi.ModTime().UTC(),
Entries: entries,
}
toc.dirs = append(toc.dirs, dir)
// Write DirInfo.
err = t.ExecuteTemplate(buf, "DirInfo", dir)
if err != nil {
return err
}
}
return nil
}
err := vfsutil.WalkFiles(fs, "/", walkFn)
return err
}
// readDirPaths reads the directory named by dirname and returns
// a sorted list of directory paths.
func readDirPaths(fs http.FileSystem, dirname string) ([]string, error) {
fis, err := vfsutil.ReadDir(fs, dirname)
if err != nil {
return nil, err
}
paths := make([]string, len(fis))
for i := range fis {
paths[i] = pathpkg.Join(dirname, fis[i].Name())
}
sort.Strings(paths)
return paths, nil
}
// writeCompressedFileInfo writes CompressedFileInfo.
// It returns errCompressedNotSmaller if compressed file is not smaller than original.
func writeCompressedFileInfo(w io.Writer, file *fileInfo, r io.Reader) error {
err := t.ExecuteTemplate(w, "CompressedFileInfo-Before", file)
if err != nil {
return err
}
sw := &stringWriter{Writer: w}
gw := gzip.NewWriter(sw)
_, err = io.Copy(gw, r)
if err != nil {
return err
}
err = gw.Close()
if err != nil {
return err
}
if sw.N >= file.UncompressedSize {
return errCompressedNotSmaller
}
err = t.ExecuteTemplate(w, "CompressedFileInfo-After", file)
return err
}
var errCompressedNotSmaller = errors.New("compressed file is not smaller than original")
// Write FileInfo.
func writeFileInfo(w io.Writer, file *fileInfo, r io.Reader) error {
err := t.ExecuteTemplate(w, "FileInfo-Before", file)
if err != nil {
return err
}
sw := &stringWriter{Writer: w}
_, err = io.Copy(sw, r)
if err != nil {
return err
}
err = t.ExecuteTemplate(w, "FileInfo-After", file)
return err
}
var t = template.Must(template.New("").Funcs(template.FuncMap{
"quote": strconv.Quote,
"comment": func(s string) (string, error) {
var buf bytes.Buffer
cw := &commentWriter{W: &buf}
_, err := io.WriteString(cw, s)
if err != nil {
return "", err
}
err = cw.Close()
return buf.String(), err
},
}).Parse(`{{define "Header"}}// Code generated by vfsgen; DO NOT EDIT.
{{with .BuildTags}}// +build {{.}}
{{end}}package {{.PackageName}}
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
pathpkg "path"
"time"
)
{{comment .VariableComment}}
var {{.VariableName}} = func() http.FileSystem {
fs := vfsgen۰FS{
{{end}}
{{define "CompressedFileInfo-Before"}} {{quote .Path}}: &vfsgen۰CompressedFileInfo{
name: {{quote .Name}},
modTime: {{template "Time" .ModTime}},
uncompressedSize: {{.UncompressedSize}},
{{/* This blank line separating compressedContent is neccessary to prevent potential gofmt issues. See issue #19. */}}
compressedContent: []byte("{{end}}{{define "CompressedFileInfo-After"}}"),
},
{{end}}
{{define "FileInfo-Before"}} {{quote .Path}}: &vfsgen۰FileInfo{
name: {{quote .Name}},
modTime: {{template "Time" .ModTime}},
content: []byte("{{end}}{{define "FileInfo-After"}}"),
},
{{end}}
{{define "DirInfo"}} {{quote .Path}}: &vfsgen۰DirInfo{
name: {{quote .Name}},
modTime: {{template "Time" .ModTime}},
},
{{end}}
{{define "DirEntries"}} }
{{range .}}{{if .Entries}} fs[{{quote .Path}}].(*vfsgen۰DirInfo).entries = []os.FileInfo{{"{"}}{{range .Entries}}
fs[{{quote .}}].(os.FileInfo),{{end}}
}
{{end}}{{end}}
return fs
}()
{{end}}
{{define "Trailer"}}
type vfsgen۰FS map[string]interface{}
func (fs vfsgen۰FS) Open(path string) (http.File, error) {
path = pathpkg.Clean("/" + path)
f, ok := fs[path]
if !ok {
return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
}
switch f := f.(type) {{"{"}}{{if .HasCompressedFile}}
case *vfsgen۰CompressedFileInfo:
gr, err := gzip.NewReader(bytes.NewReader(f.compressedContent))
if err != nil {
// This should never happen because we generate the gzip bytes such that they are always valid.
panic("unexpected error reading own gzip compressed bytes: " + err.Error())
}
return &vfsgen۰CompressedFile{
vfsgen۰CompressedFileInfo: f,
gr: gr,
}, nil{{end}}{{if .HasFile}}
case *vfsgen۰FileInfo:
return &vfsgen۰File{
vfsgen۰FileInfo: f,
Reader: bytes.NewReader(f.content),
}, nil{{end}}
case *vfsgen۰DirInfo:
return &vfsgen۰Dir{
vfsgen۰DirInfo: f,
}, nil
default:
// This should never happen because we generate only the above types.
panic(fmt.Sprintf("unexpected type %T", f))
}
}
{{if .HasCompressedFile}}
// vfsgen۰CompressedFileInfo is a static definition of a gzip compressed file.
type vfsgen۰CompressedFileInfo struct {
name string
modTime time.Time
compressedContent []byte
uncompressedSize int64
}
func (f *vfsgen۰CompressedFileInfo) Readdir(count int) ([]os.FileInfo, error) {
return nil, fmt.Errorf("cannot Readdir from file %s", f.name)
}
func (f *vfsgen۰CompressedFileInfo) Stat() (os.FileInfo, error) { return f, nil }
func (f *vfsgen۰CompressedFileInfo) GzipBytes() []byte {
return f.compressedContent
}
func (f *vfsgen۰CompressedFileInfo) Name() string { return f.name }
func (f *vfsgen۰CompressedFileInfo) Size() int64 { return f.uncompressedSize }
func (f *vfsgen۰CompressedFileInfo) Mode() os.FileMode { return 0444 }
func (f *vfsgen۰CompressedFileInfo) ModTime() time.Time { return f.modTime }
func (f *vfsgen۰CompressedFileInfo) IsDir() bool { return false }
func (f *vfsgen۰CompressedFileInfo) Sys() interface{} { return nil }
// vfsgen۰CompressedFile is an opened compressedFile instance.
type vfsgen۰CompressedFile struct {
*vfsgen۰CompressedFileInfo
gr *gzip.Reader
grPos int64 // Actual gr uncompressed position.
seekPos int64 // Seek uncompressed position.
}
func (f *vfsgen۰CompressedFile) Read(p []byte) (n int, err error) {
if f.grPos > f.seekPos {
// Rewind to beginning.
err = f.gr.Reset(bytes.NewReader(f.compressedContent))
if err != nil {
return 0, err
}
f.grPos = 0
}
if f.grPos < f.seekPos {
// Fast-forward.
_, err = io.CopyN(ioutil.Discard, f.gr, f.seekPos-f.grPos)
if err != nil {
return 0, err
}
f.grPos = f.seekPos
}
n, err = f.gr.Read(p)
f.grPos += int64(n)
f.seekPos = f.grPos
return n, err
}
func (f *vfsgen۰CompressedFile) Seek(offset int64, whence int) (int64, error) {
switch whence {
case io.SeekStart:
f.seekPos = 0 + offset
case io.SeekCurrent:
f.seekPos += offset
case io.SeekEnd:
f.seekPos = f.uncompressedSize + offset
default:
panic(fmt.Errorf("invalid whence value: %v", whence))
}
return f.seekPos, nil
}
func (f *vfsgen۰CompressedFile) Close() error {
return f.gr.Close()
}
{{else}}
// We already imported "compress/gzip" and "io/ioutil", but ended up not using them. Avoid unused import error.
var _ = gzip.Reader{}
var _ = ioutil.Discard
{{end}}{{if .HasFile}}
// vfsgen۰FileInfo is a static definition of an uncompressed file (because it's not worth gzip compressing).
type vfsgen۰FileInfo struct {
name string
modTime time.Time
content []byte
}
func (f *vfsgen۰FileInfo) Readdir(count int) ([]os.FileInfo, error) {
return nil, fmt.Errorf("cannot Readdir from file %s", f.name)
}
func (f *vfsgen۰FileInfo) Stat() (os.FileInfo, error) { return f, nil }
func (f *vfsgen۰FileInfo) NotWorthGzipCompressing() {}
func (f *vfsgen۰FileInfo) Name() string { return f.name }
func (f *vfsgen۰FileInfo) Size() int64 { return int64(len(f.content)) }
func (f *vfsgen۰FileInfo) Mode() os.FileMode { return 0444 }
func (f *vfsgen۰FileInfo) ModTime() time.Time { return f.modTime }
func (f *vfsgen۰FileInfo) IsDir() bool { return false }
func (f *vfsgen۰FileInfo) Sys() interface{} { return nil }
// vfsgen۰File is an opened file instance.
type vfsgen۰File struct {
*vfsgen۰FileInfo
*bytes.Reader
}
func (f *vfsgen۰File) Close() error {
return nil
}
{{else if not .HasCompressedFile}}
// We already imported "bytes", but ended up not using it. Avoid unused import error.
var _ = bytes.Reader{}
{{end}}
// vfsgen۰DirInfo is a static definition of a directory.
type vfsgen۰DirInfo struct {
name string
modTime time.Time
entries []os.FileInfo
}
func (d *vfsgen۰DirInfo) Read([]byte) (int, error) {
return 0, fmt.Errorf("cannot Read from directory %s", d.name)
}
func (d *vfsgen۰DirInfo) Close() error { return nil }
func (d *vfsgen۰DirInfo) Stat() (os.FileInfo, error) { return d, nil }
func (d *vfsgen۰DirInfo) Name() string { return d.name }
func (d *vfsgen۰DirInfo) Size() int64 { return 0 }
func (d *vfsgen۰DirInfo) Mode() os.FileMode { return 0755 | os.ModeDir }
func (d *vfsgen۰DirInfo) ModTime() time.Time { return d.modTime }
func (d *vfsgen۰DirInfo) IsDir() bool { return true }
func (d *vfsgen۰DirInfo) Sys() interface{} { return nil }
// vfsgen۰Dir is an opened dir instance.
type vfsgen۰Dir struct {
*vfsgen۰DirInfo
pos int // Position within entries for Seek and Readdir.
}
func (d *vfsgen۰Dir) Seek(offset int64, whence int) (int64, error) {
if offset == 0 && whence == io.SeekStart {
d.pos = 0
return 0, nil
}
return 0, fmt.Errorf("unsupported Seek in directory %s", d.name)
}
func (d *vfsgen۰Dir) Readdir(count int) ([]os.FileInfo, error) {
if d.pos >= len(d.entries) && count > 0 {
return nil, io.EOF
}
if count <= 0 || count > len(d.entries)-d.pos {
count = len(d.entries) - d.pos
}
e := d.entries[d.pos : d.pos+count]
d.pos += count
return e, nil
}
{{end}}
{{define "Time"}}
{{- if .IsZero -}}
time.Time{}
{{- else -}}
time.Date({{.Year}}, {{printf "%d" .Month}}, {{.Day}}, {{.Hour}}, {{.Minute}}, {{.Second}}, {{.Nanosecond}}, time.UTC)
{{- end -}}
{{end}}
`))
package vfsgen
import (
"fmt"
"strings"
)
// Options for vfsgen code generation.
type Options struct {
// Filename of the generated Go code output (including extension).
// If left empty, it defaults to "{{toLower .VariableName}}_vfsdata.go".
Filename string
// PackageName is the name of the package in the generated code.
// If left empty, it defaults to "main".
PackageName string
// BuildTags are the optional build tags in the generated code.
// The build tags syntax is specified by the go tool.
BuildTags string
// VariableName is the name of the http.FileSystem variable in the generated code.
// If left empty, it defaults to "assets".
VariableName string
// VariableComment is the comment of the http.FileSystem variable in the generated code.
// If left empty, it defaults to "{{.VariableName}} statically implements the virtual filesystem provided to vfsgen.".
VariableComment string
}
// fillMissing sets default values for mandatory options that are left empty.
func (opt *Options) fillMissing() {
if opt.PackageName == "" {
opt.PackageName = "main"
}
if opt.VariableName == "" {
opt.VariableName = "assets"
}
if opt.Filename == "" {
opt.Filename = fmt.Sprintf("%s_vfsdata.go", strings.ToLower(opt.VariableName))
}
if opt.VariableComment == "" {
opt.VariableComment = fmt.Sprintf("%s statically implements the virtual filesystem provided to vfsgen.", opt.VariableName)
}
}
package vfsgen
import (
"io"
)
// stringWriter writes given bytes to underlying io.Writer as a Go interpreted string literal value,
// not including double quotes. It tracks the total number of bytes written.
type stringWriter struct {
io.Writer
N int64 // Total bytes written.
}
func (sw *stringWriter) Write(p []byte) (n int, err error) {
const hex = "0123456789abcdef"
buf := []byte{'\\', 'x', 0, 0}
for _, b := range p {
buf[2], buf[3] = hex[b/16], hex[b%16]
_, err = sw.Writer.Write(buf)
if err != nil {
return n, err
}
n++
sw.N++
}
return n, nil
}
......@@ -895,6 +895,36 @@
"revision": "84bc9597164f671c0130543778228928d6865c5c",
"revisionTime": "2017-06-08T03:40:07Z"
},
{
"checksumSHA1": "fSVkjXu8SIkCLI9RTt/vnkg9M5A=",
"path": "github.com/shurcooL/httpfs/filter",
"revision": "809beceb23714880abc4a382a00c05f89d13b1cc",
"revisionTime": "2017-11-19T17:43:26Z"
},
{
"checksumSHA1": "NJs03JE5kTrf/0jMSQzn6GGnqtI=",
"path": "github.com/shurcooL/httpfs/union",
"revision": "809beceb23714880abc4a382a00c05f89d13b1cc",
"revisionTime": "2017-11-19T17:43:26Z"
},
{
"checksumSHA1": "fhngc51BJ8gGzM5b8BpSuWjCLao=",
"path": "github.com/shurcooL/httpfs/vfsutil",
"revision": "809beceb23714880abc4a382a00c05f89d13b1cc",
"revisionTime": "2017-11-19T17:43:26Z"
},
{
"checksumSHA1": "mAsjjGWwuClAdK2+Jdfupe4t4Y4=",
"path": "github.com/shurcooL/vfsgen",
"revision": "62bca832be04bd2bcaabd3b68a6b19a7ec044411",
"revisionTime": "2018-07-11T16:37:06Z"
},
{
"checksumSHA1": "STxYqRb4gnlSr3mRpT+Igfdz/kM=",
"path": "github.com/spf13/pflag",
"revision": "e57e3eeb33f795204c1ca35f56c44f83227c6e66",
"revisionTime": "2017-05-08T18:43:26Z"
},
{
"checksumSHA1": "iydUphwYqZRq3WhstEdGsbvBAKs=",
"path": "github.com/stretchr/testify/assert",
......
The `ui` package contains static files and templates used in the web UI. For
The `ui` directory contains static files and templates used in the web UI. For
easier distribution they are statically compiled into the Prometheus binary
using the go-bindata tool (c.f. Makefile).
using the vfsgen library (c.f. Makefile).
During development it is more convenient to always use the files on disk to
directly see changes without recompiling.
To make this work, set the environment variable `DEBUG=1`, run `make assets`, and then `make build`.
This will put `go-bindata` in DEBUG mode where it serves from your local filesystem.
To make this work, add `-tags dev` to the `flags` entry in `.promu.yml`, and then `make build`.
This will serve all files from your local filesystem.
This is for development purposes only.
After making changes to any file, run `make assets` (without the `DEBUG=1`) before committing to update
After making changes to any file, run `make assets` before committing to update
the generated inline version of the file.
// Copyright 2018 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build ignore
package main
import (
"log"
"github.com/prometheus/prometheus/web/ui"
"github.com/shurcooL/vfsgen"
)
func main() {
err := vfsgen.Generate(ui.Assets, vfsgen.Options{
PackageName: "ui",
BuildTags: "!dev",
VariableName: "Assets",
})
if err != nil {
log.Fatalln(err)
}
}
此差异已折叠。
此差异已折叠。
// Copyright 2018 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package ui provides the assets via a virtual filesystem.
package ui
import (
// The blank import is to make govendor happy.
_ "github.com/shurcooL/vfsgen"
)
//go:generate go run -tags=dev assets_generate.go
// Copyright 2018 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build dev
package ui
import (
"go/build"
"log"
"net/http"
"os"
"strings"
"github.com/shurcooL/httpfs/filter"
"github.com/shurcooL/httpfs/union"
)
func importPathToDir(importPath string) string {
p, err := build.Import(importPath, "", build.FindOnly)
if err != nil {
log.Fatalln(err)
}
return p.Dir
}
var static http.FileSystem = filter.Keep(
http.Dir(importPathToDir("github.com/prometheus/prometheus/web/ui/static")),
func(path string, fi os.FileInfo) bool {
return fi.IsDir() ||
(!strings.HasSuffix(path, "map.js") &&
!strings.HasSuffix(path, "/bootstrap.js") &&
!strings.HasSuffix(path, "/bootstrap-theme.css") &&
!strings.HasSuffix(path, "/bootstrap.css"))
},
)
var templates http.FileSystem = filter.Keep(
http.Dir(importPathToDir("github.com/prometheus/prometheus/web/ui/templates")),
func(path string, fi os.FileInfo) bool {
return fi.IsDir() || strings.HasSuffix(path, ".html")
},
)
// Assets contains the project's assets.
var Assets http.FileSystem = union.New(map[string]http.FileSystem{
"/templates": templates,
"/static": static,
})
......@@ -14,7 +14,6 @@
package web
import (
"bytes"
"context"
"encoding/json"
"fmt"
......@@ -262,7 +261,11 @@ func New(logger log.Logger, o *Options) *Handler {
router.Get("/consoles/*filepath", readyf(h.consoles))
router.Get("/static/*filepath", h.serveStaticAsset)
router.Get("/static/*filepath", func(w http.ResponseWriter, r *http.Request) {
r.URL.Path = filepath.Join("/static", route.Param(r.Context(), "filepath"))
fs := http.FileServer(ui.Assets)
fs.ServeHTTP(w, r)
})
if o.UserAssetsPath != "" {
router.Get("/user/*filepath", route.FileServe(o.UserAssetsPath))
......@@ -349,28 +352,6 @@ func serveDebug(w http.ResponseWriter, req *http.Request) {
}
}
func (h *Handler) serveStaticAsset(w http.ResponseWriter, req *http.Request) {
fp := route.Param(req.Context(), "filepath")
fp = filepath.Join("web/ui/static", fp)
info, err := ui.AssetInfo(fp)
if err != nil {
level.Warn(h.logger).Log("msg", "Could not get file info", "err", err, "file", fp)
w.WriteHeader(http.StatusNotFound)
return
}
file, err := ui.Asset(fp)
if err != nil {
if err != io.EOF {
level.Warn(h.logger).Log("msg", "Could not get file", "err", err, "file", fp)
}
w.WriteHeader(http.StatusNotFound)
return
}
http.ServeContent(w, req, info.Name(), info.ModTime(), bytes.NewReader(file))
}
// Ready sets Handler to be ready.
func (h *Handler) Ready() {
atomic.StoreUint32(&h.ready, 1)
......@@ -848,15 +829,32 @@ func tmplFuncs(consolesPath string, opts *Options) template_text.FuncMap {
}
func (h *Handler) getTemplate(name string) (string, error) {
baseTmpl, err := ui.Asset("web/ui/templates/_base.html")
var tmpl string
appendf := func(name string) error {
f, err := ui.Assets.Open(filepath.Join("/templates", name))
if err != nil {
return err
}
defer f.Close()
b, err := ioutil.ReadAll(f)
if err != nil {
return err
}
tmpl += string(b)
return nil
}
err := appendf("_base.html")
if err != nil {
return "", fmt.Errorf("error reading base template: %s", err)
}
pageTmpl, err := ui.Asset(filepath.Join("web/ui/templates", name))
err = appendf(name)
if err != nil {
return "", fmt.Errorf("error reading page template %s: %s", name, err)
}
return string(baseTmpl) + string(pageTmpl), nil
return tmpl, nil
}
func (h *Handler) executeTemplate(w http.ResponseWriter, name string, data interface{}) {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册