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