diff --git a/deploy/deploy.go b/deploy/deploy.go index 9a38072a7..f6b5b5785 100644 --- a/deploy/deploy.go +++ b/deploy/deploy.go @@ -33,6 +33,7 @@ import ( "github.com/dustin/go-humanize" "github.com/gobwas/glob" "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/media" "github.com/pkg/errors" "github.com/spf13/afero" jww "github.com/spf13/jwalterweatherman" @@ -51,6 +52,7 @@ type Deployer struct { target *target // the target to deploy to matchers []*matcher // matchers to apply to uploaded files + mediaTypes media.Types // Hugo's MediaType to guess ContentType ordering []*regexp.Regexp // orders uploads quiet bool // true reduces STDOUT confirm bool // true enables confirmation before making changes @@ -96,11 +98,13 @@ func New(cfg config.Provider, localFs afero.Fs) (*Deployer, error) { return nil, fmt.Errorf("deployment target %q not found", targetName) } } + return &Deployer{ localFs: localFs, target: tgt, matchers: dcfg.Matchers, ordering: dcfg.ordering, + mediaTypes: dcfg.mediaTypes, quiet: cfg.GetBool("quiet"), confirm: cfg.GetBool("confirm"), dryRun: cfg.GetBool("dryRun"), @@ -130,7 +134,7 @@ func (d *Deployer) Deploy(ctx context.Context) error { if d.target != nil { include, exclude = d.target.includeGlob, d.target.excludeGlob } - local, err := walkLocal(d.localFs, d.matchers, include, exclude) + local, err := walkLocal(d.localFs, d.matchers, include, exclude, d.mediaTypes) if err != nil { return err } @@ -322,14 +326,15 @@ type localFile struct { // gzipped before upload. UploadSize int64 - fs afero.Fs - matcher *matcher - md5 []byte // cache - gzipped bytes.Buffer // cached of gzipped contents if gzipping + fs afero.Fs + matcher *matcher + md5 []byte // cache + gzipped bytes.Buffer // cached of gzipped contents if gzipping + mediaTypes media.Types } // newLocalFile initializes a *localFile. -func newLocalFile(fs afero.Fs, nativePath, slashpath string, m *matcher) (*localFile, error) { +func newLocalFile(fs afero.Fs, nativePath, slashpath string, m *matcher, mt media.Types) (*localFile, error) { f, err := fs.Open(nativePath) if err != nil { return nil, err @@ -340,6 +345,7 @@ func newLocalFile(fs afero.Fs, nativePath, slashpath string, m *matcher) (*local SlashPath: slashpath, fs: fs, matcher: m, + mediaTypes: mt, } if m != nil && m.Gzip { // We're going to gzip the content. Do it once now, and cache the result @@ -410,10 +416,13 @@ func (lf *localFile) ContentType() string { if lf.matcher != nil && lf.matcher.ContentType != "" { return lf.matcher.ContentType } - // TODO: Hugo has a MediaType and a MediaTypes list and also a concept - // of custom MIME types. - // Use 1) The matcher 2) Hugo's MIME types 3) TypeByExtension. - return mime.TypeByExtension(filepath.Ext(lf.NativePath)) + + ext := filepath.Ext(lf.NativePath) + if mimeType, found := lf.mediaTypes.GetFirstBySuffix(strings.TrimPrefix(ext, ".")); found { + return mimeType.Type() + } + + return mime.TypeByExtension(ext) } // Force returns true if the file should be forced to re-upload based on the @@ -457,7 +466,7 @@ func knownHiddenDirectory(name string) bool { // walkLocal walks the source directory and returns a flat list of files, // using localFile.SlashPath as the map keys. -func walkLocal(fs afero.Fs, matchers []*matcher, include, exclude glob.Glob) (map[string]*localFile, error) { +func walkLocal(fs afero.Fs, matchers []*matcher, include, exclude glob.Glob, mediaTypes media.Types) (map[string]*localFile, error) { retval := map[string]*localFile{} err := afero.Walk(fs, "", func(path string, info os.FileInfo, err error) error { if err != nil { @@ -503,7 +512,7 @@ func walkLocal(fs afero.Fs, matchers []*matcher, include, exclude glob.Glob) (ma break } } - lf, err := newLocalFile(fs, path, slashpath, m) + lf, err := newLocalFile(fs, path, slashpath, m, mediaTypes) if err != nil { return err } diff --git a/deploy/deployConfig.go b/deploy/deployConfig.go index ecfabb7a4..cc2b15280 100644 --- a/deploy/deployConfig.go +++ b/deploy/deployConfig.go @@ -20,6 +20,7 @@ import ( "github.com/gobwas/glob" "github.com/gohugoio/hugo/config" hglob "github.com/gohugoio/hugo/hugofs/glob" + "github.com/gohugoio/hugo/media" "github.com/mitchellh/mapstructure" ) @@ -31,7 +32,8 @@ type deployConfig struct { Matchers []*matcher Order []string - ordering []*regexp.Regexp // compiled Order + ordering []*regexp.Regexp // compiled Order + mediaTypes media.Types } type target struct { @@ -108,7 +110,12 @@ func (m *matcher) Matches(path string) bool { // decode creates a config from a given Hugo configuration. func decodeConfig(cfg config.Provider) (deployConfig, error) { - var dcfg deployConfig + + var ( + mediaTypesConfig []map[string]interface{} + dcfg deployConfig + ) + if !cfg.IsSet(deploymentConfigKey) { return dcfg, nil } @@ -134,5 +141,14 @@ func decodeConfig(cfg config.Provider) (deployConfig, error) { } dcfg.ordering = append(dcfg.ordering, re) } + + if cfg.IsSet("mediaTypes") { + mediaTypesConfig = append(mediaTypesConfig, cfg.GetStringMap("mediaTypes")) + } + + dcfg.mediaTypes, err = media.DecodeTypes(mediaTypesConfig...) + if err != nil { + return dcfg, err + } return dcfg, nil } diff --git a/deploy/deploy_test.go b/deploy/deploy_test.go index 0ca1b3fac..0ae10b539 100644 --- a/deploy/deploy_test.go +++ b/deploy/deploy_test.go @@ -28,6 +28,7 @@ import ( "sort" "testing" + "github.com/gohugoio/hugo/media" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/spf13/afero" @@ -208,6 +209,7 @@ func TestFindDiffs(t *testing.T) { } func TestWalkLocal(t *testing.T) { + tests := map[string]struct { Given []string Expect []string @@ -246,7 +248,7 @@ func TestWalkLocal(t *testing.T) { fd.Close() } } - if got, err := walkLocal(fs, nil, nil, nil); err != nil { + if got, err := walkLocal(fs, nil, nil, nil, media.DefaultTypes); err != nil { t.Fatal(err) } else { expect := map[string]interface{}{} @@ -287,6 +289,7 @@ func TestLocalFile(t *testing.T) { Description string Path string Matcher *matcher + MediaTypesConfig []map[string]interface{} WantContent []byte WantSize int64 WantMD5 []byte @@ -344,6 +347,18 @@ func TestLocalFile(t *testing.T) { WantMD5: gzMD5[:], WantContentEncoding: "gzip", }, + { + Description: "Custom MediaType", + Path: "foo.hugo", + MediaTypesConfig: []map[string]interface{}{ + { + "hugo/custom": map[string]interface{}{ + "suffixes": []string{"hugo"}}}}, + WantContent: contentBytes, + WantSize: contentLen, + WantMD5: contentMD5[:], + WantContentType: "hugo/custom", + }, } for _, tc := range tests { @@ -352,7 +367,15 @@ func TestLocalFile(t *testing.T) { if err := afero.WriteFile(fs, tc.Path, []byte(content), os.ModePerm); err != nil { t.Fatal(err) } - lf, err := newLocalFile(fs, tc.Path, filepath.ToSlash(tc.Path), tc.Matcher) + mediaTypes := media.DefaultTypes + if len(tc.MediaTypesConfig) > 0 { + mt, err := media.DecodeTypes(tc.MediaTypesConfig...) + if err != nil { + t.Fatal(err) + } + mediaTypes = mt + } + lf, err := newLocalFile(fs, tc.Path, filepath.ToSlash(tc.Path), tc.Matcher, mediaTypes) if err != nil { t.Fatal(err) } @@ -543,6 +566,7 @@ func TestEndToEndSync(t *testing.T) { localFs: test.fs, maxDeletes: -1, bucket: test.bucket, + mediaTypes: media.DefaultTypes, } // Initial deployment should sync remote with local. @@ -629,6 +653,7 @@ func TestMaxDeletes(t *testing.T) { localFs: test.fs, maxDeletes: -1, bucket: test.bucket, + mediaTypes: media.DefaultTypes, } // Sync remote with local. @@ -702,7 +727,6 @@ func TestMaxDeletes(t *testing.T) { // TestIncludeExclude verifies that the include/exclude options for targets work. func TestIncludeExclude(t *testing.T) { ctx := context.Background() - tests := []struct { Include string Exclude string @@ -766,6 +790,7 @@ func TestIncludeExclude(t *testing.T) { maxDeletes: -1, bucket: fsTest.bucket, target: tgt, + mediaTypes: media.DefaultTypes, } // Sync remote with local. @@ -826,6 +851,7 @@ func TestIncludeExcludeRemoteDelete(t *testing.T) { localFs: fsTest.fs, maxDeletes: -1, bucket: fsTest.bucket, + mediaTypes: media.DefaultTypes, } // Initial sync to get the files on the remote @@ -865,6 +891,7 @@ func TestIncludeExcludeRemoteDelete(t *testing.T) { // In particular, MD5 hashes must be of the compressed content. func TestCompression(t *testing.T) { ctx := context.Background() + tests, cleanup, err := initFsTests() if err != nil { t.Fatal(err) @@ -877,9 +904,10 @@ func TestCompression(t *testing.T) { t.Fatal(err) } deployer := &Deployer{ - localFs: test.fs, - bucket: test.bucket, - matchers: []*matcher{{Pattern: ".*", Gzip: true, re: regexp.MustCompile(".*")}}, + localFs: test.fs, + bucket: test.bucket, + matchers: []*matcher{{Pattern: ".*", Gzip: true, re: regexp.MustCompile(".*")}}, + mediaTypes: media.DefaultTypes, } // Initial deployment should sync remote with local. @@ -935,9 +963,10 @@ func TestMatching(t *testing.T) { t.Fatal(err) } deployer := &Deployer{ - localFs: test.fs, - bucket: test.bucket, - matchers: []*matcher{{Pattern: "^subdir/aaa$", Force: true, re: regexp.MustCompile("^subdir/aaa$")}}, + localFs: test.fs, + bucket: test.bucket, + matchers: []*matcher{{Pattern: "^subdir/aaa$", Force: true, re: regexp.MustCompile("^subdir/aaa$")}}, + mediaTypes: media.DefaultTypes, } // Initial deployment to sync remote with local.