Skip to content

Commit 034e621

Browse files
committed
Add -strip-index-html
Fixes #503
1 parent 7314143 commit 034e621

File tree

11 files changed

+201
-18
lines changed

11 files changed

+201
-18
lines changed

‎README.md‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,15 @@ The list of flags from running `s3deploy -h`:
8484
regexp pattern of files to ignore when walking the local directory, repeat flag for multiple patterns, default "^(.*/)?/?.DS_Store$"
8585
-source string
8686
path of files to upload (default ".")
87+
-strip-index-html
88+
strip index.html from all directories expect for the root entry
8789
-try
8890
trial run, no remote updates
8991
-v enable verbose logging
9092
-workers int
9193
number of workers to upload files (default -1)
9294
```
9395

94-
Note that `-skip-local-dirs` and `-skip-local-files` will match against a relative path from the source directory with Unix-style path separators. The source directory is represented by `.`, the rest starts with a `/`.
95-
9696
The flags can be set in one of (in priority order):
9797

9898
1. As a flag, e.g. `s3deploy -path public/`
@@ -110,6 +110,14 @@ max-delete: "${MYVARS_MAX_DELETE@U}"
110110
111111
Note the special `@U` (_Unquoute_) syntax for the int field.
112112

113+
#### Skip local files and directories
114+
115+
The options `-skip-local-dirs` and `-skip-local-files` will match against a relative path from the source directory with Unix-style path separators. The source directory is represented by `.`, the rest starts with a `/`.
116+
117+
#### Strip index.html
118+
119+
The option `-strip-index-html` strips index.html from all directories expect for the root entry. This matches the option with (almost) same name in [hugo deploy](https://gohugo.io/hosting-and-deployment/hugo-deploy/). This simplifies the cloud configuration needed for some use cases, such as CloudFront distributions with S3 bucket origins. See this [PR](https://github.com/gohugoio/hugo/pull/12608) for more information.
120+
113121
### Routes
114122

115123
The `.s3deploy.yml` configuration file can also contain one or more routes. A route matches files given a regexp. Each route can apply:

‎lib/cloudfront.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ func (c *cloudFrontClient) normalizeInvalidationPaths(
176176
var maxlevels int
177177

178178
for _, p := range paths {
179-
p = path.Clean(p)
179+
p = pathClean(p)
180180
if !strings.HasPrefix(p, "/") {
181181
p = "/" + p
182182
}

‎lib/cloudfront_test.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ func TestReduceInvalidationPaths(t *testing.T) {
2727
c.Assert(client.normalizeInvalidationPaths("", 5, false, "/index.html"), qt.DeepEquals, []string{"/"})
2828
c.Assert(client.normalizeInvalidationPaths("", 5, true, "/a", "/b"), qt.DeepEquals, []string{"/*"})
2929
c.Assert(client.normalizeInvalidationPaths("root", 5, true, "/a", "/b"), qt.DeepEquals, []string{"/root/*"})
30+
c.Assert(client.normalizeInvalidationPaths("root", 5, false, "/root/b/"), qt.DeepEquals, []string{"/root/b/"})
3031

3132
rootPlusMany := append([]string{"/index.html", "/styles.css"}, createFiles("css", false, 20)...)
3233
normalized := client.normalizeInvalidationPaths("", 5, false, rootPlusMany...)

‎lib/config.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ type Config struct {
7474
MaxDelete int
7575
ACL string
7676
PublicReadACL bool
77+
StripIndexHTML bool
7778
Verbose bool
7879
Silent bool
7980
Force bool
@@ -283,6 +284,7 @@ func flagsToConfig(f *flag.FlagSet) *Config {
283284
f.StringVar(&cfg.ConfigFile, "config", ".s3deploy.yml", "optional config file")
284285
f.IntVar(&cfg.MaxDelete, "max-delete", 256, "maximum number of files to delete per deploy")
285286
f.BoolVar(&cfg.PublicReadACL, "public-access", false, "DEPRECATED: please set -acl='public-read'")
287+
f.BoolVar(&cfg.StripIndexHTML, "strip-index-html", false, "strip index.html from all directories expect for the root entry")
286288
f.StringVar(&cfg.ACL, "acl", "", "provide an ACL for uploaded objects. to make objects public, set to 'public-read'. all possible values are listed here: https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl (default \"private\")")
287289
f.BoolVar(&cfg.Force, "force", false, "upload even if the etags match")
288290
f.Var(&cfg.Ignore, "ignore", "regexp pattern for ignoring files, repeat flag for multiple patterns,")

‎lib/deployer.go‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ func (d *Deployer) printf(format string, a ...interface{}) {
152152
}
153153

154154
func (d *Deployer) enqueueUpload(ctx context.Context, f *osFile) {
155-
d.Printf("%s (%s) %s ", f.relPath, f.reason, up)
155+
d.Printf("%s (%s) %s ", f.keyPath, f.reason, up)
156156
select {
157157
case <-ctx.Done():
158158
case d.filesToUpload <- f:
@@ -197,9 +197,9 @@ func (d *Deployer) plan(ctx context.Context) error {
197197
up := true
198198
reason := reasonNotFound
199199

200-
bucketPath := f.relPath
200+
bucketPath := f.keyPath
201201
if d.cfg.BucketPath != "" {
202-
bucketPath = path.Join(d.cfg.BucketPath, bucketPath)
202+
bucketPath = pathJoin(d.cfg.BucketPath, bucketPath)
203203
}
204204

205205
if remoteFile, ok := remoteFiles[bucketPath]; ok {
@@ -274,7 +274,7 @@ func (d *Deployer) walk(ctx context.Context, basePath string, files chan<- *osFi
274274
return nil
275275
}
276276

277-
f, err := newOSFile(d.cfg.fileConf.Routes, d.cfg.BucketPath, rel, abs, info)
277+
f, err := newOSFile(d.cfg, rel, abs, info)
278278
if err != nil {
279279
return err
280280
}

‎lib/files.go‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
"mime"
1616
"net/http"
1717
"os"
18-
"path"
1918
"path/filepath"
2019
"regexp"
2120
"sync"
@@ -55,6 +54,7 @@ type localFile interface {
5554

5655
type osFile struct {
5756
relPath string
57+
keyPath string // may be different from relPath if StripIndexHTML is set.
5858

5959
// Filled when BucketPath is provided. Will store files in a sub-path
6060
// of the target file store.
@@ -77,9 +77,9 @@ type osFile struct {
7777

7878
func (f *osFile) Key() string {
7979
if f.targetRoot != "" {
80-
return path.Join(f.targetRoot, f.relPath)
80+
return pathJoin(f.targetRoot, f.keyPath)
8181
}
82-
return f.relPath
82+
return f.keyPath
8383
}
8484

8585
func (f *osFile) UploadReason() uploadReason {
@@ -177,7 +177,10 @@ func (f *osFile) shouldThisReplace(other file) (bool, uploadReason) {
177177
return false, ""
178178
}
179179

180-
func newOSFile(routes routes, targetRoot, relPath, absPath string, fi os.FileInfo) (*osFile, error) {
180+
func newOSFile(cfg *Config, relPath, absPath string, fi os.FileInfo) (*osFile, error) {
181+
targetRoot := cfg.BucketPath
182+
routes := cfg.fileConf.Routes
183+
181184
relPath = filepath.ToSlash(relPath)
182185

183186
file, err := os.Open(absPath)
@@ -211,7 +214,12 @@ func newOSFile(routes routes, targetRoot, relPath, absPath string, fi os.FileInf
211214
mFile = memfile.New(b)
212215
}
213216

214-
of := &osFile{route: route, f: mFile, targetRoot: targetRoot, absPath: absPath, relPath: relPath, size: size}
217+
keyPath := relPath
218+
if cfg.StripIndexHTML {
219+
keyPath = trimIndexHTML(keyPath)
220+
}
221+
222+
of := &osFile{route: route, f: mFile, targetRoot: targetRoot, absPath: absPath, relPath: relPath, keyPath: keyPath, size: size}
215223

216224
if err := of.initContentType(peek); err != nil {
217225
return nil, err

‎lib/files_test.go‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,5 +91,14 @@ func openTestFile(name string) (*osFile, error) {
9191
return nil, err
9292
}
9393

94-
return newOSFile(nil, "", relPath, absPath, fi)
94+
args := []string{
95+
"-bucket=mybucket",
96+
}
97+
98+
cfg, err := ConfigFromArgs(args)
99+
if err != nil {
100+
return nil, err
101+
}
102+
103+
return newOSFile(cfg, relPath, absPath, fi)
95104
}

‎lib/url.go‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package lib
22

3+
import (
4+
"path"
5+
"strings"
6+
)
7+
38
// [RFC 1738](https://www.ietf.org/rfc/rfc1738.txt)
49
// §2.2
510
func shouldEscape(c byte) bool {
@@ -71,3 +76,35 @@ func pathEscapeRFC1738(s string) string {
7176
}
7277
return string(t)
7378
}
79+
80+
// Like path.Join, but preserves trailing slash..
81+
func pathJoin(elem ...string) string {
82+
if len(elem) == 0 {
83+
return ""
84+
}
85+
hadSlash := strings.HasSuffix(elem[len(elem)-1], "/")
86+
p := path.Join(elem...)
87+
if hadSlash {
88+
p += "/"
89+
}
90+
return p
91+
}
92+
93+
// pathClean works like path.Clean but will always preserve a trailing slash.
94+
func pathClean(p string) string {
95+
hadSlash := strings.HasSuffix(p, "/")
96+
p = path.Clean(p)
97+
if hadSlash && !strings.HasSuffix(p, "/") {
98+
p += "/"
99+
}
100+
return p
101+
}
102+
103+
// trimIndexHTML remaps paths matching "<dir>/index.html" to "<dir>/".
104+
func trimIndexHTML(p string) string {
105+
const suffix = "/index.html"
106+
if strings.HasSuffix(p, suffix) {
107+
return p[:len(p)-len(suffix)+1]
108+
}
109+
return p
110+
}

‎lib/url_test.go‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,39 @@ func TestPathEscapeRFC1738(t *testing.T) {
3232
c.Assert(actual, qt.Equals, tc.expected)
3333
}
3434
}
35+
36+
func TestPathJoin(t *testing.T) {
37+
c := qt.New(t)
38+
39+
testCases := []struct {
40+
elements []string
41+
expected string
42+
}{
43+
{[]string{"a", "b"}, "a/b"},
44+
{[]string{"a", "b/"}, "a/b/"},
45+
{[]string{"/a", "b/"}, "/a/b/"},
46+
}
47+
48+
for _, tc := range testCases {
49+
actual := pathJoin(tc.elements...)
50+
c.Assert(actual, qt.Equals, tc.expected)
51+
}
52+
}
53+
54+
func TestPathClean(t *testing.T) {
55+
c := qt.New(t)
56+
57+
testCases := []struct {
58+
in string
59+
expected string
60+
}{
61+
{"/path/", "/path/"},
62+
{"/path/./", "/path/"},
63+
{"/path", "/path"},
64+
}
65+
66+
for _, tc := range testCases {
67+
actual := pathClean(tc.in)
68+
c.Assert(actual, qt.Equals, tc.expected)
69+
}
70+
}

‎main_test.go‎

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@ package main
77

88
import (
99
"bytes"
10+
"context"
1011
"fmt"
1112
"net/http"
1213
"os"
1314
"sort"
1415
"strings"
1516
"testing"
1617

18+
"github.com/aws/aws-sdk-go-v2/aws"
19+
"github.com/aws/aws-sdk-go-v2/credentials"
20+
"github.com/aws/aws-sdk-go-v2/service/s3"
1721
"github.com/oklog/ulid/v2"
1822

1923
"github.com/rogpeppe/go-internal/testscript"
@@ -24,7 +28,6 @@ const s3IntegrationTestHttpRoot = "http://s3deployintegrationtest.s3-website.eu-
2428
func TestIntegration(t *testing.T) {
2529
if os.Getenv("S3DEPLOY_TEST_KEY") == "" {
2630
t.Skip("S3DEPLOY_TEST_KEY not set")
27-
2831
}
2932
p := commonTestScriptsParam
3033
p.Dir = "testscripts"
@@ -39,7 +42,6 @@ func TestUnfinished(t *testing.T) {
3942
p := commonTestScriptsParam
4043
p.Dir = "testscripts/unfinished"
4144
testscript.Run(t, p)
42-
4345
}
4446

4547
func TestMain(m *testing.M) {
@@ -57,21 +59,75 @@ func TestMain(m *testing.M) {
5759
)
5860
}
5961

62+
const (
63+
testBucket = "s3deployintegrationtest"
64+
testRegion = "eu-north-1"
65+
)
66+
6067
func setup(env *testscript.Env) error {
6168
env.Setenv("S3DEPLOY_TEST_KEY", os.Getenv("S3DEPLOY_TEST_KEY"))
6269
env.Setenv("S3DEPLOY_TEST_SECRET", os.Getenv("S3DEPLOY_TEST_SECRET"))
63-
env.Setenv("S3DEPLOY_TEST_BUCKET", "s3deployintegrationtest")
64-
env.Setenv("S3DEPLOY_TEST_REGION", "eu-north-1")
70+
env.Setenv("S3DEPLOY_TEST_BUCKET", testBucket)
71+
env.Setenv("S3DEPLOY_TEST_REGION", testRegion)
6572
env.Setenv("S3DEPLOY_TEST_URL", s3IntegrationTestHttpRoot)
6673
env.Setenv("S3DEPLOY_TEST_ID", strings.ToLower(ulid.Make().String()))
6774
return nil
6875
}
6976

77+
func gtKeySecret(ts *testscript.TestScript) (string, string) {
78+
key := ts.Getenv("S3DEPLOY_TEST_KEY")
79+
secret := ts.Getenv("S3DEPLOY_TEST_SECRET")
80+
if key == "" || secret == "" {
81+
ts.Fatalf("S3DEPLOY_TEST_KEY and S3DEPLOY_TEST_SECRET must be set")
82+
}
83+
return key, secret
84+
}
85+
7086
var commonTestScriptsParam = testscript.Params{
7187
Setup: func(env *testscript.Env) error {
7288
return setup(env)
7389
},
7490
Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){
91+
"s3get": func(ts *testscript.TestScript, neg bool, args []string) {
92+
key := args[0]
93+
testKey, testSecret := gtKeySecret(ts)
94+
config := aws.Config{
95+
Region: testRegion,
96+
Credentials: credentials.NewStaticCredentialsProvider(testKey, testSecret, os.Getenv("AWS_SESSION_TOKEN")),
97+
}
98+
99+
client := s3.NewFromConfig(config)
100+
101+
obj, err := client.GetObject(
102+
context.Background(),
103+
&s3.GetObjectInput{
104+
Bucket: aws.String(testBucket),
105+
Key: aws.String(key),
106+
},
107+
)
108+
if err != nil {
109+
ts.Fatalf("failed to get object: %v", err)
110+
}
111+
defer obj.Body.Close()
112+
var buf bytes.Buffer
113+
if _, err := buf.ReadFrom(obj.Body); err != nil {
114+
ts.Fatalf("failed to read object: %v", err)
115+
}
116+
var (
117+
contentEncoding string
118+
contentType string
119+
)
120+
if obj.ContentEncoding != nil {
121+
contentEncoding = *obj.ContentEncoding
122+
}
123+
if obj.ContentType != nil {
124+
contentType = *obj.ContentType
125+
}
126+
fmt.Fprintf(ts.Stdout(), "s3get %s: ContentEncoding: %s ContentType: %s %s\n", key, contentEncoding, contentType, buf.String())
127+
for k, v := range obj.Metadata {
128+
fmt.Fprintf(ts.Stdout(), "s3get metadata: %s: %s\n", k, v)
129+
}
130+
},
75131

76132
// head executes HTTP HEAD on the given URL and prints the response status code and
77133
// headers to stdout.
@@ -91,7 +147,6 @@ var commonTestScriptsParam = testscript.Params{
91147
}
92148
sort.Strings(headers)
93149
fmt.Fprintf(ts.Stdout(), "Headers: %s", strings.Join(headers, ";"))
94-
95150
},
96151

97152
// append appends to a file with a leaading newline.

0 commit comments

Comments
 (0)