Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

deploy: Add stripIndexHtml target option #12608

Merged
merged 1 commit into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,14 @@ func (d *Deployer) Deploy(ctx context.Context) error {

// Load local files from the source directory.
var include, exclude glob.Glob
var mappath func(string) string
if d.target != nil {
include, exclude = d.target.IncludeGlob, d.target.ExcludeGlob
if d.target.StripIndexHTML {
mappath = stripIndexHTML
}
}
local, err := d.walkLocal(d.localFs, d.cfg.Matchers, include, exclude, d.mediaTypes)
local, err := d.walkLocal(d.localFs, d.cfg.Matchers, include, exclude, d.mediaTypes, mappath)
if err != nil {
return err
}
Expand Down Expand Up @@ -483,7 +487,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 (d *Deployer) walkLocal(fs afero.Fs, matchers []*deployconfig.Matcher, include, exclude glob.Glob, mediaTypes media.Types) (map[string]*localFile, error) {
func (d *Deployer) walkLocal(fs afero.Fs, matchers []*deployconfig.Matcher, include, exclude glob.Glob, mediaTypes media.Types, mappath func(string) string) (map[string]*localFile, error) {
retval := map[string]*localFile{}
err := afero.Walk(fs, "", func(path string, info os.FileInfo, err error) error {
if err != nil {
Expand Down Expand Up @@ -529,6 +533,11 @@ func (d *Deployer) walkLocal(fs afero.Fs, matchers []*deployconfig.Matcher, incl
break
}
}
// Apply any additional modifications to the local path, to map it to
// the remote path.
if mappath != nil {
slashpath = mappath(slashpath)
}
lf, err := newLocalFile(fs, path, slashpath, m, mediaTypes)
if err != nil {
return err
Expand All @@ -542,6 +551,15 @@ func (d *Deployer) walkLocal(fs afero.Fs, matchers []*deployconfig.Matcher, incl
return retval, nil
}

// stripIndexHTML remaps keys matching "<dir>/index.html" to "<dir>/".
func stripIndexHTML(slashpath string) string {
const suffix = "/index.html"
if strings.HasSuffix(slashpath, suffix) {
return slashpath[:len(slashpath)-len(suffix)+1]
}
return slashpath
}

// walkRemote walks the target bucket and returns a flat list.
func (d *Deployer) walkRemote(ctx context.Context, bucket *blob.Bucket, include, exclude glob.Glob) (map[string]*blob.ListObject, error) {
retval := map[string]*blob.ListObject{}
Expand Down
69 changes: 66 additions & 3 deletions deploy/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,9 @@ func TestFindDiffs(t *testing.T) {

func TestWalkLocal(t *testing.T) {
tests := map[string]struct {
Given []string
Expect []string
Given []string
Expect []string
MapPath func(string) string
}{
"Empty": {
Given: []string{},
Expand All @@ -235,6 +236,11 @@ func TestWalkLocal(t *testing.T) {
Given: []string{"file.txt", ".hidden_dir/file.txt", ".well-known/file.txt"},
Expect: []string{"file.txt", ".well-known/file.txt"},
},
"StripIndexHTML": {
Given: []string{"index.html", "file.txt", "dir/index.html", "dir/file.txt"},
Expect: []string{"index.html", "file.txt", "dir/", "dir/file.txt"},
MapPath: stripIndexHTML,
},
}

for desc, tc := range tests {
Expand All @@ -254,7 +260,7 @@ func TestWalkLocal(t *testing.T) {
}
}
d := newDeployer()
if got, err := d.walkLocal(fs, nil, nil, nil, media.DefaultTypes); err != nil {
if got, err := d.walkLocal(fs, nil, nil, nil, media.DefaultTypes, tc.MapPath); err != nil {
t.Fatal(err)
} else {
expect := map[string]any{}
Expand All @@ -274,6 +280,63 @@ func TestWalkLocal(t *testing.T) {
}
}

func TestStripIndexHTML(t *testing.T) {
tests := map[string]struct {
Input string
Output string
}{
"Unmapped": {Input: "normal_file.txt", Output: "normal_file.txt"},
"Stripped": {Input: "directory/index.html", Output: "directory/"},
"NoSlash": {Input: "prefix_index.html", Output: "prefix_index.html"},
"Root": {Input: "index.html", Output: "index.html"},
}
for desc, tc := range tests {
t.Run(desc, func(t *testing.T) {
got := stripIndexHTML(tc.Input)
if got != tc.Output {
t.Errorf("got %q, expect %q", got, tc.Output)
}
})
}
}

func TestStripIndexHTMLMatcher(t *testing.T) {
// StripIndexHTML should not affect matchers.
fs := afero.NewMemMapFs()
if err := fs.Mkdir("dir", 0o755); err != nil {
t.Fatal(err)
}
for _, name := range []string{"index.html", "dir/index.html", "file.txt"} {
if fd, err := fs.Create(name); err != nil {
t.Fatal(err)
} else {
fd.Close()
}
}
d := newDeployer()
const pattern = `\.html$`
matcher := &deployconfig.Matcher{Pattern: pattern, Gzip: true, Re: regexp.MustCompile(pattern)}
if got, err := d.walkLocal(fs, []*deployconfig.Matcher{matcher}, nil, nil, media.DefaultTypes, stripIndexHTML); err != nil {
t.Fatal(err)
} else {
for _, name := range []string{"index.html", "dir/"} {
lf := got[name]
if lf == nil {
t.Errorf("missing file %q", name)
} else if lf.matcher == nil {
t.Errorf("file %q has nil matcher, expect %q", name, pattern)
}
}
const name = "file.txt"
lf := got[name]
if lf == nil {
t.Errorf("missing file %q", name)
} else if lf.matcher != nil {
t.Errorf("file %q has matcher %q, expect nil", name, lf.matcher.Pattern)
}
}
}

func TestLocalFile(t *testing.T) {
const (
content = "hello world!"
Expand Down
5 changes: 5 additions & 0 deletions deploy/deployconfig/deployConfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ type Target struct {
// Parsed versions of Include/Exclude.
IncludeGlob glob.Glob `json:"-"`
ExcludeGlob glob.Glob `json:"-"`

// If true, any local path matching <dir>/index.html will be mapped to the
// remote path <dir>/. This does not affect the top-level index.html file,
// since that would result in an empty path.
StripIndexHTML bool
}

func (tgt *Target) ParseIncludeExclude() error {
Expand Down
10 changes: 10 additions & 0 deletions docs/content/en/hosting-and-deployment/hugo-deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,15 @@ URL = "<FILL ME IN>"
#include = "**.html" # would only include files with ".html" suffix
#exclude = "**.{jpg, png}" # would exclude files with ".jpg" or ".png" suffix

# Map any file named "<dir>/index.html" to the remote file "<dir>/". This does
# not affect the root "index.html" file, and it does not affect matchers below.
# This works when deploying to key-value cloud storage systems, such as Amazon
# S3 (general purpose buckets, not directory buckets), Google Cloud Storage, and
# Azure Blob Storage. This makes it so the canonical URL will match the object
# key in cloud storage, except for the root index.html file.
#
#stripIndexHTML = true


#######################
[[deployment.matchers]]
Expand All @@ -195,6 +204,7 @@ URL = "<FILL ME IN>"

# See https://golang.org/pkg/regexp/syntax/ for pattern syntax.
# Pattern searching is stopped on first match.
# This is not affected by stripIndexHTML, above.
pattern = "<FILL ME IN>"

# If true, Hugo will gzip the file before uploading it to the bucket.
Expand Down
Loading