Skip to content

making the statik tool vendorable #86

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Is this a crazy idea? No, not necessarily. If you're building a tool that has a
Install the command line tool first.

go get github.com/rakyll/statik

statik is a tiny program that reads a directory and generates a source file that contains its contents. The generated source file registers the directory contents to be used by statik file system.

The command below will walk on the public path and generate a package called `statik` under the current working directory.
Expand Down Expand Up @@ -42,4 +42,30 @@ Visit http://localhost:8080/public/path/to/file to see your file.

There is also a working example under [example](https://github.com/rakyll/statik/tree/master/example) directory, follow the instructions to build and run it.

Alternative to `go get`, you can also vendor statik to use in `go generate`:

Create a file gen.go:

```
package main

import (
"github.com/rakyll/statik/cmd"
)

func main() {
cmd.Main()
}
```

and use that in the `go generate` command:

```
//go:generate go run ./gen/gen.go -src=./public -tags !dev

```

This way you can control which tag/commit of the tool you want to use.


Note: The idea and the implementation are hijacked from [camlistore](http://camlistore.org/). I decided to decouple it from its codebase due to the fact I'm actively in need of a similar solution for many of my projects.
234 changes: 234 additions & 0 deletions cmd/statik.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
package cmd

import (
"archive/zip"
"bytes"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"time"
)

const (
nameSourceFile = "statik.go"
)

var namePackage string

var (
flagSrc = flag.String("src", path.Join(".", "public"), "The path of the source directory.")
flagDest = flag.String("dest", ".", "The destination path of the generated package.")
flagNoMtime = flag.Bool("m", false, "Ignore modification times on files.")
flagNoCompress = flag.Bool("Z", false, "Do not use compression to shrink the files.")
flagForce = flag.Bool("f", false, "Overwrite destination file if it already exists.")
flagTags = flag.String("tags", "", "Write build constraint tags")
flagPkg = flag.String("p", "statik", "Name of the generated package")
flagPkgCmt = flag.String("c", "Package statik contains static assets.", "The package comment. An empty value disables this comment.\n")
)

// mtimeDate holds the arbitrary mtime that we assign to files when
// flagNoMtime is set.
var mtimeDate = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)

func Main() {
flag.Parse()

namePackage = *flagPkg

file, err := generateSource(*flagSrc)
if err != nil {
exitWithError(err)
}

destDir := path.Join(*flagDest, namePackage)
err = os.MkdirAll(destDir, 0755)
if err != nil {
exitWithError(err)
}

err = rename(file.Name(), path.Join(destDir, nameSourceFile))
if err != nil {
exitWithError(err)
}
}

// rename tries to os.Rename, but fall backs to copying from src
// to dest and unlink the source if os.Rename fails.
func rename(src, dest string) error {
// Try to rename generated source.
if err := os.Rename(src, dest); err == nil {
return nil
}
// If the rename failed (might do so due to temporary file residing on a
// different device), try to copy byte by byte.
rc, err := os.Open(src)
if err != nil {
return err
}
defer func() {
rc.Close()
os.Remove(src) // ignore the error, source is in tmp.
}()

if _, err = os.Stat(dest); !os.IsNotExist(err) {
if *flagForce {
if err = os.Remove(dest); err != nil {
return fmt.Errorf("file %q could not be deleted", dest)
}
} else {
return fmt.Errorf("file %q already exists; use -f to overwrite", dest)
}
}

wc, err := os.Create(dest)
if err != nil {
return err
}
defer wc.Close()

if _, err = io.Copy(wc, rc); err != nil {
// Delete remains of failed copy attempt.
os.Remove(dest)
}
return err
}

// Walks on the source path and generates source code
// that contains source directory's contents as zip contents.
// Generates source registers generated zip contents data to
// be read by the statik/fs HTTP file system.
func generateSource(srcPath string) (file *os.File, err error) {
var (
buffer bytes.Buffer
zipWriter io.Writer
)

zipWriter = &buffer
f, err := ioutil.TempFile("", namePackage)
if err != nil {
return
}

zipWriter = io.MultiWriter(zipWriter, f)
defer f.Close()

w := zip.NewWriter(zipWriter)
if err = filepath.Walk(srcPath, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
// Ignore directories and hidden files.
// No entry is needed for directories in a zip file.
// Each file is represented with a path, no directory
// entities are required to build the hierarchy.
if fi.IsDir() || strings.HasPrefix(fi.Name(), ".") {
return nil
}
relPath, err := filepath.Rel(srcPath, path)
if err != nil {
return err
}
b, err := ioutil.ReadFile(path)
if err != nil {
return err
}
fHeader, err := zip.FileInfoHeader(fi)
if err != nil {
return err
}
if *flagNoMtime {
// Always use the same modification time so that
// the output is deterministic with respect to the file contents.
// Do NOT use fHeader.Modified as it only works on go >= 1.10
fHeader.SetModTime(mtimeDate)
}
fHeader.Name = filepath.ToSlash(relPath)
if !*flagNoCompress {
fHeader.Method = zip.Deflate
}
f, err := w.CreateHeader(fHeader)
if err != nil {
return err
}
_, err = f.Write(b)
return err
}); err != nil {
return
}
if err = w.Close(); err != nil {
return
}

var tags string
if *flagTags != "" {
tags = "\n// +build " + *flagTags + "\n"
}

var comment string
if *flagPkgCmt != "" {
comment = "\n" + commentLines(*flagPkgCmt)
}

// then embed it as a quoted string
var qb bytes.Buffer
fmt.Fprintf(&qb, `// Code generated by statik. DO NOT EDIT.
%s%s
package %s

import (
"github.com/rakyll/statik/fs"
)

func init() {
data := "`, tags, comment, namePackage)
FprintZipData(&qb, buffer.Bytes())
fmt.Fprint(&qb, `"
fs.Register(data)
}
`)

if err = ioutil.WriteFile(f.Name(), qb.Bytes(), 0644); err != nil {
return
}
return f, nil
}

// FprintZipData converts zip binary contents to a string literal.
func FprintZipData(dest *bytes.Buffer, zipData []byte) {
for _, b := range zipData {
if b == '\n' {
dest.WriteString(`\n`)
continue
}
if b == '\\' {
dest.WriteString(`\\`)
continue
}
if b == '"' {
dest.WriteString(`\"`)
continue
}
if (b >= 32 && b <= 126) || b == '\t' {
dest.WriteByte(b)
continue
}
fmt.Fprintf(dest, "\\x%02x", b)
}
}

// comment lines prefixes each line in lines with "// ".
func commentLines(lines string) string {
lines = "// " + strings.Replace(lines, "\n", "\n// ", -1)
return lines
}

// Prints out the error message and exists with a non-success signal.
func exitWithError(err error) {
fmt.Println(err)
os.Exit(1)
}
2 changes: 1 addition & 1 deletion example/main.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:generate statik -src=./public
//go:generate go run ./statik/gen/gen.go -src=./public

package main

Expand Down
9 changes: 9 additions & 0 deletions example/statik/gen/gen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package main

import (
"github.com/rakyll/statik/cmd"
)

func main() {
cmd.Main()
}
2 changes: 1 addition & 1 deletion example/statik/statik.go

Large diffs are not rendered by default.

Loading