1// Copyright 2014 Google Inc. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// Package contains a program that generates code to register
16// a directory and its contents as zip data for statik file system.
17package main
18
19import (
20	"archive/zip"
21	"bytes"
22	"flag"
23	"fmt"
24	"io"
25	"io/ioutil"
26	"os"
27	"path"
28	"path/filepath"
29	"strings"
30	"time"
31)
32
33const (
34	nameSourceFile = "statik.go"
35)
36
37var namePackage string
38
39var (
40	flagSrc        = flag.String("src", path.Join(".", "public"), "The path of the source directory.")
41	flagDest       = flag.String("dest", ".", "The destination path of the generated package.")
42	flagNoMtime    = flag.Bool("m", false, "Ignore modification times on files.")
43	flagNoCompress = flag.Bool("Z", false, "Do not use compression to shrink the files.")
44	flagForce      = flag.Bool("f", false, "Overwrite destination file if it already exists.")
45	flagTags       = flag.String("tags", "", "Write build constraint tags")
46	flagPkg        = flag.String("p", "statik", "Name of the generated package")
47	flagPkgCmt     = flag.String("c", "Package statik contains static assets.", "The package comment. An empty value disables this comment.\n")
48)
49
50// mtimeDate holds the arbitrary mtime that we assign to files when
51// flagNoMtime is set.
52var mtimeDate = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)
53
54func main() {
55	flag.Parse()
56
57	namePackage = *flagPkg
58
59	file, err := generateSource(*flagSrc)
60	if err != nil {
61		exitWithError(err)
62	}
63
64	destDir := path.Join(*flagDest, namePackage)
65	err = os.MkdirAll(destDir, 0755)
66	if err != nil {
67		exitWithError(err)
68	}
69
70	err = rename(file.Name(), path.Join(destDir, nameSourceFile))
71	if err != nil {
72		exitWithError(err)
73	}
74}
75
76// rename tries to os.Rename, but fall backs to copying from src
77// to dest and unlink the source if os.Rename fails.
78func rename(src, dest string) error {
79	// Try to rename generated source.
80	if err := os.Rename(src, dest); err == nil {
81		return nil
82	}
83	// If the rename failed (might do so due to temporary file residing on a
84	// different device), try to copy byte by byte.
85	rc, err := os.Open(src)
86	if err != nil {
87		return err
88	}
89	defer func() {
90		rc.Close()
91		os.Remove(src) // ignore the error, source is in tmp.
92	}()
93
94	if _, err = os.Stat(dest); !os.IsNotExist(err) {
95		if *flagForce {
96			if err = os.Remove(dest); err != nil {
97				return fmt.Errorf("file %q could not be deleted", dest)
98			}
99		} else {
100			return fmt.Errorf("file %q already exists; use -f to overwrite", dest)
101		}
102	}
103
104	wc, err := os.Create(dest)
105	if err != nil {
106		return err
107	}
108	defer wc.Close()
109
110	if _, err = io.Copy(wc, rc); err != nil {
111		// Delete remains of failed copy attempt.
112		os.Remove(dest)
113	}
114	return err
115}
116
117// Walks on the source path and generates source code
118// that contains source directory's contents as zip contents.
119// Generates source registers generated zip contents data to
120// be read by the statik/fs HTTP file system.
121func generateSource(srcPath string) (file *os.File, err error) {
122	var (
123		buffer    bytes.Buffer
124		zipWriter io.Writer
125	)
126
127	zipWriter = &buffer
128	f, err := ioutil.TempFile("", namePackage)
129	if err != nil {
130		return
131	}
132
133	zipWriter = io.MultiWriter(zipWriter, f)
134	defer f.Close()
135
136	w := zip.NewWriter(zipWriter)
137	if err = filepath.Walk(srcPath, func(path string, fi os.FileInfo, err error) error {
138		if err != nil {
139			return err
140		}
141		// Ignore directories and hidden files.
142		// No entry is needed for directories in a zip file.
143		// Each file is represented with a path, no directory
144		// entities are required to build the hierarchy.
145		if fi.IsDir() || strings.HasPrefix(fi.Name(), ".") {
146			return nil
147		}
148		relPath, err := filepath.Rel(srcPath, path)
149		if err != nil {
150			return err
151		}
152		b, err := ioutil.ReadFile(path)
153		if err != nil {
154			return err
155		}
156		fHeader, err := zip.FileInfoHeader(fi)
157		if err != nil {
158			return err
159		}
160		if *flagNoMtime {
161			// Always use the same modification time so that
162			// the output is deterministic with respect to the file contents.
163			// Do NOT use fHeader.Modified as it only works on go >= 1.10
164			fHeader.SetModTime(mtimeDate)
165		}
166		fHeader.Name = filepath.ToSlash(relPath)
167		if !*flagNoCompress {
168			fHeader.Method = zip.Deflate
169		}
170		f, err := w.CreateHeader(fHeader)
171		if err != nil {
172			return err
173		}
174		_, err = f.Write(b)
175		return err
176	}); err != nil {
177		return
178	}
179	if err = w.Close(); err != nil {
180		return
181	}
182
183	var tags string
184	if *flagTags != "" {
185		tags = "\n// +build " + *flagTags + "\n"
186	}
187
188	var comment string
189	if *flagPkgCmt != "" {
190		comment = "\n" + commentLines(*flagPkgCmt)
191	}
192
193	// then embed it as a quoted string
194	var qb bytes.Buffer
195	fmt.Fprintf(&qb, `// Code generated by statik. DO NOT EDIT.
196%s%s
197package %s
198
199import (
200	"github.com/rakyll/statik/fs"
201)
202
203func init() {
204	data := "`, tags, comment, namePackage)
205	FprintZipData(&qb, buffer.Bytes())
206	fmt.Fprint(&qb, `"
207	fs.Register(data)
208}
209`)
210
211	if err = ioutil.WriteFile(f.Name(), qb.Bytes(), 0644); err != nil {
212		return
213	}
214	return f, nil
215}
216
217// FprintZipData converts zip binary contents to a string literal.
218func FprintZipData(dest *bytes.Buffer, zipData []byte) {
219	for _, b := range zipData {
220		if b == '\n' {
221			dest.WriteString(`\n`)
222			continue
223		}
224		if b == '\\' {
225			dest.WriteString(`\\`)
226			continue
227		}
228		if b == '"' {
229			dest.WriteString(`\"`)
230			continue
231		}
232		if (b >= 32 && b <= 126) || b == '\t' {
233			dest.WriteByte(b)
234			continue
235		}
236		fmt.Fprintf(dest, "\\x%02x", b)
237	}
238}
239
240// comment lines prefixes each line in lines with "// ".
241func commentLines(lines string) string {
242	lines = "// " + strings.Replace(lines, "\n", "\n// ", -1)
243	return lines
244}
245
246// Prints out the error message and exists with a non-success signal.
247func exitWithError(err error) {
248	fmt.Println(err)
249	os.Exit(1)
250}
251