// Copyright 2013 bee 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 bale import ( "bytes" "compress/gzip" "fmt" "io" "os" "path" "path/filepath" "runtime" "strings" "github.com/beego/bee/v2/cmd/commands" "github.com/beego/bee/v2/cmd/commands/version" "github.com/beego/bee/v2/config" beeLogger "github.com/beego/bee/v2/logger" "github.com/beego/bee/v2/utils" ) var CmdBale = &commands.Command{ UsageLine: "bale", Short: "Transforms non-Go files to Go source files", Long: `Bale command compress all the static files in to a single binary file. This is useful to not have to carry static files including js, css, images and views when deploying a Web application. It will auto-generate an unpack function to the main package then run it during the runtime. This is mainly used for zealots who are requiring 100% Go code. `, PreRun: func(cmd *commands.Command, args []string) { version.ShowShortVersionBanner() }, Run: runBale, } func init() { commands.AvailableCommands = append(commands.AvailableCommands, CmdBale) } func runBale(cmd *commands.Command, args []string) int { os.RemoveAll("bale") os.Mkdir("bale", os.ModePerm) // Pack and compress data for _, p := range config.Conf.Bale.Dirs { if !utils.IsExist(p) { beeLogger.Log.Warnf("Skipped directory: %s", p) continue } beeLogger.Log.Infof("Packaging directory: %s", p) filepath.Walk(p, walkFn) } // Generate auto-uncompress function. buf := new(bytes.Buffer) buf.WriteString(fmt.Sprintf(BaleHeader, config.Conf.Bale.Import, strings.Join(resFiles, "\",\n\t\t\""), strings.Join(resFiles, ",\n\t\tbale.R"))) fw, err := os.Create("bale.go") if err != nil { beeLogger.Log.Fatalf("Failed to create file: %s", err) } defer fw.Close() _, err = fw.Write(buf.Bytes()) if err != nil { beeLogger.Log.Fatalf("Failed to write data: %s", err) } beeLogger.Log.Success("Baled resources successfully!") return 0 } const ( // BaleHeader ... BaleHeader = `package main import( "os" "strings" "path" "%s" ) func isExist(path string) bool { _, err := os.Stat(path) return err == nil || os.IsExist(err) } func init() { files := []string{ "%s", } funcs := []func() []byte{ bale.R%s, } for i, f := range funcs { fp := getFilePath(files[i]) if !isExist(fp) { saveFile(fp, f()) } } } func getFilePath(name string) string { name = strings.Replace(name, "_4_", "/", -1) name = strings.Replace(name, "_3_", " ", -1) name = strings.Replace(name, "_2_", "-", -1) name = strings.Replace(name, "_1_", ".", -1) name = strings.Replace(name, "_0_", "_", -1) return name } func saveFile(filePath string, b []byte) (int, error) { os.MkdirAll(path.Dir(filePath), os.ModePerm) fw, err := os.Create(filePath) if err != nil { return 0, err } defer fw.Close() return fw.Write(b) } ` ) var resFiles = make([]string, 0, 10) func walkFn(resPath string, info os.FileInfo, _ error) error { if info.IsDir() || filterSuffix(resPath) { return nil } // Open resource files fr, err := os.Open(resPath) if err != nil { beeLogger.Log.Fatalf("Failed to read file: %s", err) } // Convert path resPath = strings.Replace(resPath, "_", "_0_", -1) resPath = strings.Replace(resPath, ".", "_1_", -1) resPath = strings.Replace(resPath, "-", "_2_", -1) resPath = strings.Replace(resPath, " ", "_3_", -1) sep := "/" if runtime.GOOS == "windows" { sep = "\\" } resPath = strings.Replace(resPath, sep, "_4_", -1) // Create corresponding Go source files os.MkdirAll(path.Dir(resPath), os.ModePerm) fw, err := os.Create("bale/" + resPath + ".go") if err != nil { beeLogger.Log.Fatalf("Failed to create file: %s", err) } defer fw.Close() // Write header fmt.Fprintf(fw, Header, resPath) // Copy and compress data gz := gzip.NewWriter(&ByteWriter{Writer: fw}) io.Copy(gz, fr) gz.Close() // Write footer. fmt.Fprint(fw, Footer) resFiles = append(resFiles, resPath) return nil } func filterSuffix(name string) bool { for _, s := range config.Conf.Bale.IngExt { if strings.HasSuffix(name, s) { return true } } return false } const ( // Header ... Header = `package bale import( "bytes" "compress/gzip" "io" ) func R%s() []byte { gz, err := gzip.NewReader(bytes.NewBuffer([]byte{` // Footer ... Footer = ` })) if err != nil { panic("Unpack resources failed: " + err.Error()) } var b bytes.Buffer io.Copy(&b, gz) gz.Close() return b.Bytes() }` ) var newline = []byte{'\n'} // ByteWriter ... type ByteWriter struct { io.Writer c int } func (w *ByteWriter) Write(p []byte) (n int, err error) { if len(p) == 0 { return } for n = range p { if w.c%12 == 0 { w.Writer.Write(newline) w.c = 0 } fmt.Fprintf(w.Writer, "0x%02x,", p[n]) w.c++ } n++ return }