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{
 | 
			
		||||
	Use:   "pack",
 | 
			
		||||
	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 {
 | 
			
		||||
		// convert "out" to an absolute path, so that it will still
 | 
			
		||||
		// 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
 | 
			
		||||
		// files, and we'll build a spec from them
 | 
			
		||||
		if spec == "" {
 | 
			
		||||
| 
						 | 
				
			
			@ -58,12 +82,16 @@ var packCmd = &cobra.Command{
 | 
			
		|||
				return errors.New("need --yaml, " +
 | 
			
		||||
					"or one or more filenames")
 | 
			
		||||
			}
 | 
			
		||||
			err = PackFiles(c, args, out)
 | 
			
		||||
			err = PackFiles2(c, args, ctGlobs, out)
 | 
			
		||||
		} else {
 | 
			
		||||
			if len(args) != 0 {
 | 
			
		||||
				return errors.New("cannot specify files " +
 | 
			
		||||
					"when using --yaml")
 | 
			
		||||
			}
 | 
			
		||||
			if ctGlobs != nil {
 | 
			
		||||
				return errors.New("cannot specify --content-type " +
 | 
			
		||||
					"when using --yaml")
 | 
			
		||||
			}
 | 
			
		||||
			err = PackSpec(c, spec, out)
 | 
			
		||||
		}
 | 
			
		||||
		if err != nil {
 | 
			
		||||
| 
						 | 
				
			
			@ -83,13 +111,20 @@ func init() {
 | 
			
		|||
		"YAML specification file (if not present, just pack files)")
 | 
			
		||||
	packCmd.Flags().StringP("chdir", "C", "",
 | 
			
		||||
		"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 {
 | 
			
		||||
	return PackFiles2(c, args, nil, out)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func PackFiles2(c *cobra.Command, args []string, ctGlobs ctGlobList, out string) error {
 | 
			
		||||
	ftp, err := filesFromList(args)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	ctGlobs.ApplyContentTypes(ftp)
 | 
			
		||||
 | 
			
		||||
	return doPack(ftp, out)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue