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