cmd/htpacker: add --content-type flag
This allows overriding the content-type of files being packed using the ad-hoc method (not YAML spec).
This commit is contained in:
		
							parent
							
								
									565a269cef
								
							
						
					
					
						commit
						d827d8aace
					
				| 
						 | 
					@ -0,0 +1,105 @@
 | 
				
			||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"src.lwithers.me.uk/go/htpack/cmd/htpacker/packer"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ctGlobEntry struct {
 | 
				
			||||||
 | 
						pattern, contentType string
 | 
				
			||||||
 | 
						pathComponents       int
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ctGlobList []ctGlobEntry
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func parseGlobs(flags []string) (ctGlobList, error) {
 | 
				
			||||||
 | 
						var ctGlobs ctGlobList
 | 
				
			||||||
 | 
						for _, flag := range flags {
 | 
				
			||||||
 | 
							// split pattern:content-type
 | 
				
			||||||
 | 
							pos := strings.LastIndexByte(flag, ':')
 | 
				
			||||||
 | 
							if pos == -1 {
 | 
				
			||||||
 | 
								return nil, &parseGlobError{
 | 
				
			||||||
 | 
									Value: flag,
 | 
				
			||||||
 | 
									Err:   "must be pattern:content-type",
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							pattern, ct := flag[:pos], flag[pos+1:]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// patterns starting with "/" must match the entire directory
 | 
				
			||||||
 | 
							// prefix; otherwise, an arbitrary number of path components are
 | 
				
			||||||
 | 
							// allowed prior to the prefix
 | 
				
			||||||
 | 
							var pathComponents int
 | 
				
			||||||
 | 
							if strings.HasPrefix(pattern, "/") {
 | 
				
			||||||
 | 
								pathComponents = -1
 | 
				
			||||||
 | 
								pattern = strings.TrimPrefix(pattern, "/")
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								pathComponents = 1 + strings.Count(pattern, "/")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// test that the pattern's syntax is valid
 | 
				
			||||||
 | 
							if _, err := filepath.Match(pattern, "test"); err != nil {
 | 
				
			||||||
 | 
								return nil, &parseGlobError{
 | 
				
			||||||
 | 
									Value: flag,
 | 
				
			||||||
 | 
									Err:   err.Error(),
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							ctGlobs = append(ctGlobs, ctGlobEntry{
 | 
				
			||||||
 | 
								pattern:        pattern,
 | 
				
			||||||
 | 
								contentType:    ct,
 | 
				
			||||||
 | 
								pathComponents: pathComponents,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ctGlobs, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ApplyContentTypes will scan the list of files to pack, matching by filename,
 | 
				
			||||||
 | 
					// and on match will apply the given content type.
 | 
				
			||||||
 | 
					func (ctGlobs ctGlobList) ApplyContentTypes(ftp packer.FilesToPack) {
 | 
				
			||||||
 | 
						for name := range ftp {
 | 
				
			||||||
 | 
							for _, entry := range ctGlobs {
 | 
				
			||||||
 | 
								testName := trimPathComponents(name, entry.pathComponents)
 | 
				
			||||||
 | 
								matched, _ := filepath.Match(entry.pattern, testName)
 | 
				
			||||||
 | 
								if matched {
 | 
				
			||||||
 | 
									f := ftp[name]
 | 
				
			||||||
 | 
									f.ContentType = entry.contentType
 | 
				
			||||||
 | 
									ftp[name] = f
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func trimPathComponents(name string, components int) string {
 | 
				
			||||||
 | 
						name = strings.TrimPrefix(name, "/") // FilesToPack keys = absolute path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// if we are matching the full prefix, don't otherwise manipulate the
 | 
				
			||||||
 | 
						// name
 | 
				
			||||||
 | 
						if components < 0 {
 | 
				
			||||||
 | 
							return name
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// otherwise, trim the number of components remaining in the path so
 | 
				
			||||||
 | 
						// that we are only matching the trailing path components from the
 | 
				
			||||||
 | 
						// FilesToPack key
 | 
				
			||||||
 | 
						parts := 1 + strings.Count(name, "/")
 | 
				
			||||||
 | 
						for ; parts > components; parts-- {
 | 
				
			||||||
 | 
							pos := strings.IndexByte(name, '/')
 | 
				
			||||||
 | 
							name = name[pos+1:]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return name
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// parseGlobError is returned from parseGlobs on error.
 | 
				
			||||||
 | 
					type parseGlobError struct {
 | 
				
			||||||
 | 
						Value string
 | 
				
			||||||
 | 
						Err   string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (pge *parseGlobError) Error() string {
 | 
				
			||||||
 | 
						return fmt.Sprintf("--content-type entry %q: %s", pge.Value, pge.Err)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,109 @@
 | 
				
			||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"src.lwithers.me.uk/go/htpack/cmd/htpacker/packer"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestParseGlobs(t *testing.T) {
 | 
				
			||||||
 | 
						ctGlobs, err := parseGlobs([]string{
 | 
				
			||||||
 | 
							"*.foo:text/html",
 | 
				
			||||||
 | 
							"*.bar:text/plain",
 | 
				
			||||||
 | 
							"baz/qux/*.js:application/javascript",
 | 
				
			||||||
 | 
							"/abs/file:image/png",
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						check := func(pos int, pattern, contentType string, pathComponents int) {
 | 
				
			||||||
 | 
							if pos >= len(ctGlobs) {
 | 
				
			||||||
 | 
								t.Errorf("entry %d not present", pos)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if pattern != ctGlobs[pos].pattern {
 | 
				
			||||||
 | 
								t.Errorf("entry %d: expected pattern %q but got %q",
 | 
				
			||||||
 | 
									pos, pattern, ctGlobs[pos].pattern)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if contentType != ctGlobs[pos].contentType {
 | 
				
			||||||
 | 
								t.Errorf("entry %d: expected content type %q but got %q",
 | 
				
			||||||
 | 
									pos, contentType, ctGlobs[pos].contentType)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if pathComponents != ctGlobs[pos].pathComponents {
 | 
				
			||||||
 | 
								t.Errorf("entry %d: expected num. path components %d but got %d",
 | 
				
			||||||
 | 
									pos, pathComponents, ctGlobs[pos].pathComponents)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						check(0, "*.foo", "text/html", 1)
 | 
				
			||||||
 | 
						check(1, "*.bar", "text/plain", 1)
 | 
				
			||||||
 | 
						check(2, "baz/qux/*.js", "application/javascript", 3)
 | 
				
			||||||
 | 
						check(3, "abs/file", "image/png", -1)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestParseGlobsErrSep(t *testing.T) {
 | 
				
			||||||
 | 
						const badValue = "hello/dave.js" // missing ":" separator
 | 
				
			||||||
 | 
						_, err := parseGlobs([]string{badValue})
 | 
				
			||||||
 | 
						switch err := err.(type) {
 | 
				
			||||||
 | 
						case *parseGlobError:
 | 
				
			||||||
 | 
							if err.Value != badValue {
 | 
				
			||||||
 | 
								t.Errorf("expected value %q but got %q", badValue, err.Value)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case nil:
 | 
				
			||||||
 | 
							t.Fatal("expected error")
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							t.Errorf("unexpected error type %T (value %v)", err, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestParseGlobsErrPattern(t *testing.T) {
 | 
				
			||||||
 | 
						const badValue = "[-z]:foo/bar" // malformed character class
 | 
				
			||||||
 | 
						_, err := parseGlobs([]string{badValue})
 | 
				
			||||||
 | 
						switch err := err.(type) {
 | 
				
			||||||
 | 
						case *parseGlobError:
 | 
				
			||||||
 | 
							if err.Value != badValue {
 | 
				
			||||||
 | 
								t.Errorf("expected value %q but got %q", badValue, err.Value)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case nil:
 | 
				
			||||||
 | 
							t.Fatal("expected error")
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							t.Errorf("unexpected error type %T (value %v)", err, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestApplyContentTypes(t *testing.T) {
 | 
				
			||||||
 | 
						// XXX: we program our _expectation_ of content-type into the Filename field
 | 
				
			||||||
 | 
						ftp := packer.FilesToPack{
 | 
				
			||||||
 | 
							"foo.txt":     packer.FileToPack{Filename: "text/plain"},
 | 
				
			||||||
 | 
							"baz/foo.txt": packer.FileToPack{Filename: "text/plain"},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							"baz/qux.png":     packer.FileToPack{Filename: "image/png"},
 | 
				
			||||||
 | 
							"foo/qux.png":     packer.FileToPack{},
 | 
				
			||||||
 | 
							"foo/baz/qux.png": packer.FileToPack{Filename: "image/png"},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							"bar.jpeg":         packer.FileToPack{},
 | 
				
			||||||
 | 
							"foo/baz/bar.jpeg": packer.FileToPack{},
 | 
				
			||||||
 | 
							"baz/bar.jpeg":     packer.FileToPack{Filename: "image/jpeg"},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctGlobs, err := parseGlobs([]string{
 | 
				
			||||||
 | 
							"*.txt:text/plain",         // should match anywhere
 | 
				
			||||||
 | 
							"baz/qux.png:image/png",    // won't match /foo/qux.png
 | 
				
			||||||
 | 
							"/baz/bar.jpeg:image/jpeg", // exact prefix match
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctGlobs.ApplyContentTypes(ftp)
 | 
				
			||||||
 | 
						for k, v := range ftp {
 | 
				
			||||||
 | 
							if v.Filename != v.ContentType {
 | 
				
			||||||
 | 
								t.Errorf("filename %q: expected content type %q but got %q",
 | 
				
			||||||
 | 
									k, v.Filename, v.ContentType)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -16,6 +16,20 @@ import (
 | 
				
			||||||
var packCmd = &cobra.Command{
 | 
					var packCmd = &cobra.Command{
 | 
				
			||||||
	Use:   "pack",
 | 
						Use:   "pack",
 | 
				
			||||||
	Short: "creates a packfile from a YAML spec or set of files/dirs",
 | 
						Short: "creates a packfile from a YAML spec or set of files/dirs",
 | 
				
			||||||
 | 
						Long: `When given a YAML spec file (a template for which can be generated
 | 
				
			||||||
 | 
					with the "yaml" command), files will be packed exactly as per the spec. The
 | 
				
			||||||
 | 
					--content-type flag cannot be used and no extra files can be specified.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					When given a list of files and directories to pack, the content type for each
 | 
				
			||||||
 | 
					file will be automatically detected. It is possible to override the content
 | 
				
			||||||
 | 
					type by specifying one or more --content-type flags. These take an argument in
 | 
				
			||||||
 | 
					the form "pattern:content/type". The pattern is matched using common glob
 | 
				
			||||||
 | 
					(* = wildcard), very similar to .gitignore. If the pattern contains any
 | 
				
			||||||
 | 
					directory names, these must match the final components of the file to pack's
 | 
				
			||||||
 | 
					path. If the pattern starts with a "/", then the full path must be matched
 | 
				
			||||||
 | 
					exactly.
 | 
				
			||||||
 | 
					`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	RunE: func(c *cobra.Command, args []string) error {
 | 
						RunE: func(c *cobra.Command, args []string) error {
 | 
				
			||||||
		// convert "out" to an absolute path, so that it will still
 | 
							// convert "out" to an absolute path, so that it will still
 | 
				
			||||||
		// work after chdir
 | 
							// work after chdir
 | 
				
			||||||
| 
						 | 
					@ -51,6 +65,16 @@ var packCmd = &cobra.Command{
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// parse content-type globs
 | 
				
			||||||
 | 
							ctGlobList, err := c.Flags().GetStringArray("content-type")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							ctGlobs, err := parseGlobs(ctGlobList)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// if "spec" is not present, then we expect a list of input
 | 
							// if "spec" is not present, then we expect a list of input
 | 
				
			||||||
		// files, and we'll build a spec from them
 | 
							// files, and we'll build a spec from them
 | 
				
			||||||
		if spec == "" {
 | 
							if spec == "" {
 | 
				
			||||||
| 
						 | 
					@ -58,12 +82,16 @@ var packCmd = &cobra.Command{
 | 
				
			||||||
				return errors.New("need --yaml, " +
 | 
									return errors.New("need --yaml, " +
 | 
				
			||||||
					"or one or more filenames")
 | 
										"or one or more filenames")
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			err = PackFiles(c, args, out)
 | 
								err = PackFiles2(c, args, ctGlobs, out)
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			if len(args) != 0 {
 | 
								if len(args) != 0 {
 | 
				
			||||||
				return errors.New("cannot specify files " +
 | 
									return errors.New("cannot specify files " +
 | 
				
			||||||
					"when using --yaml")
 | 
										"when using --yaml")
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
								if ctGlobs != nil {
 | 
				
			||||||
 | 
									return errors.New("cannot specify --content-type " +
 | 
				
			||||||
 | 
										"when using --yaml")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
			err = PackSpec(c, spec, out)
 | 
								err = PackSpec(c, spec, out)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
| 
						 | 
					@ -83,13 +111,20 @@ func init() {
 | 
				
			||||||
		"YAML specification file (if not present, just pack files)")
 | 
							"YAML specification file (if not present, just pack files)")
 | 
				
			||||||
	packCmd.Flags().StringP("chdir", "C", "",
 | 
						packCmd.Flags().StringP("chdir", "C", "",
 | 
				
			||||||
		"Change to directory before searching for input files")
 | 
							"Change to directory before searching for input files")
 | 
				
			||||||
 | 
						packCmd.Flags().StringArrayP("content-type", "", nil,
 | 
				
			||||||
 | 
							"Override content type for pattern, e.g. \"*.foo=bar/baz\" (like .gitignore)")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func PackFiles(c *cobra.Command, args []string, out string) error {
 | 
					func PackFiles(c *cobra.Command, args []string, out string) error {
 | 
				
			||||||
 | 
						return PackFiles2(c, args, nil, out)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func PackFiles2(c *cobra.Command, args []string, ctGlobs ctGlobList, out string) error {
 | 
				
			||||||
	ftp, err := filesFromList(args)
 | 
						ftp, err := filesFromList(args)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						ctGlobs.ApplyContentTypes(ftp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return doPack(ftp, out)
 | 
						return doPack(ftp, out)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue