From 84ea7c3673eaf6cb5ebdebfffcf619bec422da22 Mon Sep 17 00:00:00 2001 From: Laurence Withers Date: Fri, 29 Mar 2019 09:18:07 +0000 Subject: [PATCH] cmd/htpacker: lots more WIP --- cmd/htpacker/inspector.go | 20 +++-- cmd/htpacker/main.go | 2 +- cmd/htpacker/pack.go | 52 +++++++++--- cmd/htpacker/yaml.go | 172 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 228 insertions(+), 18 deletions(-) create mode 100644 cmd/htpacker/yaml.go diff --git a/cmd/htpacker/inspector.go b/cmd/htpacker/inspector.go index 81da2dd..a621699 100644 --- a/cmd/htpacker/inspector.go +++ b/cmd/htpacker/inspector.go @@ -50,14 +50,20 @@ func Inspect(filename string) error { fmt.Printf(" • %s\n"+ " · Etag: %s\n"+ " · Content type: %s\n"+ - " · Uncompressed: %s (offset %d)\n"+ - " · Gzipped: %s (offset %d)\n"+ - " · Brotli: %s (offset %d)\n", + " · Uncompressed: %s (offset %d)\n", path, info.Etag, info.ContentType, - printSize(info.Uncompressed.Length), info.Uncompressed.Offset, - printSize(info.Gzip.Length), info.Gzip.Offset, - printSize(info.Brotli.Length), info.Brotli.Offset, - ) + printSize(info.Uncompressed.Length), + info.Uncompressed.Offset) + + if info.Gzip != nil { + fmt.Printf(" · Gzipped: %s (offset %d)\n", + printSize(info.Gzip.Length), info.Gzip.Offset) + } + + if info.Brotli != nil { + fmt.Printf(" · Brotli: %s (offset %d)\n", + printSize(info.Brotli.Length), info.Brotli.Offset) + } } } return err diff --git a/cmd/htpacker/main.go b/cmd/htpacker/main.go index ff8c6df..8370878 100644 --- a/cmd/htpacker/main.go +++ b/cmd/htpacker/main.go @@ -18,7 +18,7 @@ arguments.`, func main() { rootCmd.AddCommand(packCmd) - //rootCmd.AddCommand(yamlCmd) + rootCmd.AddCommand(yamlCmd) rootCmd.AddCommand(inspectCmd) if err := rootCmd.Execute(); err != nil { diff --git a/cmd/htpacker/pack.go b/cmd/htpacker/pack.go index ea14d26..b67a619 100644 --- a/cmd/htpacker/pack.go +++ b/cmd/htpacker/pack.go @@ -5,6 +5,7 @@ import ( "fmt" "io/ioutil" "os" + "path/filepath" "github.com/lwithers/htpack/packer" "github.com/spf13/cobra" @@ -15,23 +16,54 @@ var packCmd = &cobra.Command{ Use: "pack", Short: "creates a packfile from a YAML spec or set of files/dirs", RunE: func(c *cobra.Command, args []string) error { - spec, err := c.Flags().GetString("spec") + // convert "out" to an absolute path, so that it will still + // work after chdir + out, err := c.Flags().GetString("out") + if err != nil { + return err + } + out, err = filepath.Abs(out) if err != nil { return err } + // if "spec" is present, convert to an absolute path + spec, err := c.Flags().GetString("spec") + if err != nil { + return err + } + if spec != "" { + spec, err = filepath.Abs(spec) + if err != nil { + return err + } + } + + // chdir if required + chdir, err := c.Flags().GetString("chdir") + if err != nil { + return err + } + if chdir != "" { + if err = os.Chdir(chdir); err != nil { + return err + } + } + + // if "spec" is not present, then we expect a list of input + // files, and we'll build a spec from them if spec == "" { if len(args) == 0 { return errors.New("need --yaml, " + "or one or more filenames") } - err = PackFiles(c, args) + err = PackFiles(c, args, out) } else { if len(args) != 0 { return errors.New("cannot specify files " + "when using --yaml") } - err = PackSpec(c, spec) + err = PackSpec(c, spec, out) } if err != nil { fmt.Fprintln(os.Stderr, err) @@ -52,12 +84,15 @@ func init() { "Change to directory before searching for input files") } -func PackFiles(c *cobra.Command, args []string) error { - // TODO - return errors.New("not implemented yet") +func PackFiles(c *cobra.Command, args []string, out string) error { + ftp, err := filesFromList(args) + if err != nil { + return err + } + return packer.Pack(ftp, out) } -func PackSpec(c *cobra.Command, spec string) error { +func PackSpec(c *cobra.Command, spec, out string) error { raw, err := ioutil.ReadFile(spec) if err != nil { return err @@ -68,8 +103,5 @@ func PackSpec(c *cobra.Command, spec string) error { return fmt.Errorf("parsing YAML spec %s: %v", spec, err) } - // TODO: chdir - - out, _ := c.Flags().GetString("out") return packer.Pack(ftp, out) } diff --git a/cmd/htpacker/yaml.go b/cmd/htpacker/yaml.go new file mode 100644 index 0000000..7273810 --- /dev/null +++ b/cmd/htpacker/yaml.go @@ -0,0 +1,172 @@ +package main + +import ( + "errors" + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/lwithers/htpack/packer" + "github.com/spf13/cobra" + yaml "gopkg.in/yaml.v2" +) + +var yamlCmd = &cobra.Command{ + Use: "yaml", + Short: "Build YAML spec from list of files/dirs", + Long: `Generates a YAML specification from a list of files and directories. +The specification is suitable for passing to pack. + +File names will be mapped as follows: + • if you specify a file, it will appear be served as "/filename"; + • if you specify a directory, its contents will be merged into "/", such that a + directory with contents "a", "b", and "c/d" will cause entries "/a", "/b" and + "/c/d" to be served. +`, + RunE: func(c *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("must specify one or more files/directories") + } + + // convert "out" to absolute path, in case we need to chdir + out, err := c.Flags().GetString("out") + if err != nil { + return err + } + out, err = filepath.Abs(out) + if err != nil { + return err + } + + // chdir if required + chdir, err := c.Flags().GetString("chdir") + if err != nil { + return err + } + if chdir != "" { + if err = os.Chdir(chdir); err != nil { + return err + } + } + + if err := MakeYaml(args, out); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + return nil + }, +} + +func init() { + yamlCmd.Flags().StringP("out", "O", "", + "Output filename") + yamlCmd.MarkFlagRequired("out") + yamlCmd.Flags().StringP("chdir", "C", "", + "Change to directory before searching for input files") +} + +func MakeYaml(args []string, out string) error { + ftp, err := filesFromList(args) + if err != nil { + return err + } + + raw, err := yaml.Marshal(ftp) + if err != nil { + return fmt.Errorf("failed to marshal %T to YAML: %v", ftp, err) + } + + return ioutil.WriteFile(out, raw, 0666) +} + +func filesFromList(args []string) (packer.FilesToPack, error) { + ftp := make(packer.FilesToPack) + + // NB: we don't use filepath.Walk since: + // (a) we don't care about lexical order; just do it quick + // (b) we want to dereference symlinks + for _, arg := range args { + if err := filesFromListR(arg, arg, ftp); err != nil { + return nil, err + } + } + return ftp, nil +} + +func filesFromListR(prefix, arg string, ftp packer.FilesToPack) error { + f, err := os.Open(arg) + if err != nil { + return err + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + return err + } + + switch { + case fi.Mode().IsDir(): + // readdir + fnames, err := f.Readdirnames(0) // 0 ⇒ everything + if err != nil { + return err + } + for _, fname := range fnames { + fullname := filepath.Join(arg, fname) + if err = filesFromListR(prefix, fullname, ftp); err != nil { + return err + } + } + return nil + + case fi.Mode().IsRegular(): + // sniff content type + buf := make([]byte, 512) + n, err := f.Read(buf) + if err != nil { + return err + } + buf = buf[:n] + ctype := http.DetectContentType(buf) + + // augmented rules for JS / CSS / etc. + switch { + case strings.HasPrefix(ctype, "text/plain"): + switch filepath.Ext(arg) { + case ".css": + ctype = "text/css" + case ".js": + ctype = "application/javascript" + case ".json": + ctype = "application/json" + } + + case strings.HasPrefix(ctype, "text/xml"): + switch filepath.Ext(arg) { + case ".svg": + ctype = "image/svg+xml" + } + } + + // pack + srvName := strings.TrimPrefix(arg, prefix) + if srvName == "" { + srvName = filepath.Base(arg) + } + if !strings.HasPrefix(srvName, "/") { + srvName = "/" + srvName + } + ftp[srvName] = packer.FileToPack{ + Filename: arg, + ContentType: ctype, + } + return nil + + default: + return fmt.Errorf("%s: not file/dir (mode %x)", arg, fi.Mode()) + } +}