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