diff --git a/README.md b/README.md index fd517b8..c5d0b69 100644 --- a/README.md +++ b/README.md @@ -91,8 +91,6 @@ The list of flags from running `s3deploy -h`: number of workers to upload files (default -1) ``` -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 `/`. - The flags can be set in one of (in priority order): 1. As a flag, e.g. `s3deploy -path public/` @@ -110,6 +108,10 @@ max-delete: "${MYVARS_MAX_DELETE@U}" Note the special `@U` (_Unquoute_) syntax for the int field. +#### Skip local files and directories + +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 `/`. + ### Routes The `.s3deploy.yml` configuration file can also contain one or more routes. A route matches files given a regexp. Each route can apply: diff --git a/lib/cloudfront.go b/lib/cloudfront.go index 5a52738..57d775a 100644 --- a/lib/cloudfront.go +++ b/lib/cloudfront.go @@ -176,7 +176,7 @@ func (c *cloudFrontClient) normalizeInvalidationPaths( var maxlevels int for _, p := range paths { - p = path.Clean(p) + p = pathClean(p) if !strings.HasPrefix(p, "/") { p = "/" + p } diff --git a/lib/cloudfront_test.go b/lib/cloudfront_test.go index 389bc2f..6b16354 100644 --- a/lib/cloudfront_test.go +++ b/lib/cloudfront_test.go @@ -27,6 +27,7 @@ func TestReduceInvalidationPaths(t *testing.T) { c.Assert(client.normalizeInvalidationPaths("", 5, false, "/index.html"), qt.DeepEquals, []string{"/"}) c.Assert(client.normalizeInvalidationPaths("", 5, true, "/a", "/b"), qt.DeepEquals, []string{"/*"}) c.Assert(client.normalizeInvalidationPaths("root", 5, true, "/a", "/b"), qt.DeepEquals, []string{"/root/*"}) + c.Assert(client.normalizeInvalidationPaths("root", 5, false, "/root/b/"), qt.DeepEquals, []string{"/root/b/"}) rootPlusMany := append([]string{"/index.html", "/styles.css"}, createFiles("css", false, 20)...) normalized := client.normalizeInvalidationPaths("", 5, false, rootPlusMany...) diff --git a/lib/config.go b/lib/config.go index 2b524b7..a7bace7 100644 --- a/lib/config.go +++ b/lib/config.go @@ -74,6 +74,7 @@ type Config struct { MaxDelete int ACL string PublicReadACL bool + StripIndexHTML bool Verbose bool Silent bool Force bool @@ -283,6 +284,7 @@ func flagsToConfig(f *flag.FlagSet) *Config { f.StringVar(&cfg.ConfigFile, "config", ".s3deploy.yml", "optional config file") f.IntVar(&cfg.MaxDelete, "max-delete", 256, "maximum number of files to delete per deploy") f.BoolVar(&cfg.PublicReadACL, "public-access", false, "DEPRECATED: please set -acl='public-read'") + f.BoolVar(&cfg.StripIndexHTML, "strip-index-html", false, "strip index.html from all directories expect for the root entry") 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\")") f.BoolVar(&cfg.Force, "force", false, "upload even if the etags match") f.Var(&cfg.Ignore, "ignore", "regexp pattern for ignoring files, repeat flag for multiple patterns,") diff --git a/lib/deployer.go b/lib/deployer.go index af52c15..3556138 100644 --- a/lib/deployer.go +++ b/lib/deployer.go @@ -152,7 +152,7 @@ func (d *Deployer) printf(format string, a ...interface{}) { } func (d *Deployer) enqueueUpload(ctx context.Context, f *osFile) { - d.Printf("%s (%s) %s ", f.relPath, f.reason, up) + d.Printf("%s (%s) %s ", f.keyPath, f.reason, up) select { case <-ctx.Done(): case d.filesToUpload <- f: @@ -197,9 +197,9 @@ func (d *Deployer) plan(ctx context.Context) error { up := true reason := reasonNotFound - bucketPath := f.relPath + bucketPath := f.keyPath if d.cfg.BucketPath != "" { - bucketPath = path.Join(d.cfg.BucketPath, bucketPath) + bucketPath = pathJoin(d.cfg.BucketPath, bucketPath) } if remoteFile, ok := remoteFiles[bucketPath]; ok { @@ -274,7 +274,7 @@ func (d *Deployer) walk(ctx context.Context, basePath string, files chan<- *osFi return nil } - f, err := newOSFile(d.cfg.fileConf.Routes, d.cfg.BucketPath, rel, abs, info) + f, err := newOSFile(d.cfg, rel, abs, info) if err != nil { return err } diff --git a/lib/files.go b/lib/files.go index 0cb3e9f..20231b1 100644 --- a/lib/files.go +++ b/lib/files.go @@ -15,7 +15,6 @@ import ( "mime" "net/http" "os" - "path" "path/filepath" "regexp" "sync" @@ -55,6 +54,7 @@ type localFile interface { type osFile struct { relPath string + keyPath string // may be different from relPath if StripIndexHTML is set. // Filled when BucketPath is provided. Will store files in a sub-path // of the target file store. @@ -77,9 +77,9 @@ type osFile struct { func (f *osFile) Key() string { if f.targetRoot != "" { - return path.Join(f.targetRoot, f.relPath) + return pathJoin(f.targetRoot, f.keyPath) } - return f.relPath + return f.keyPath } func (f *osFile) UploadReason() uploadReason { @@ -177,7 +177,10 @@ func (f *osFile) shouldThisReplace(other file) (bool, uploadReason) { return false, "" } -func newOSFile(routes routes, targetRoot, relPath, absPath string, fi os.FileInfo) (*osFile, error) { +func newOSFile(cfg *Config, relPath, absPath string, fi os.FileInfo) (*osFile, error) { + targetRoot := cfg.BucketPath + routes := cfg.fileConf.Routes + relPath = filepath.ToSlash(relPath) file, err := os.Open(absPath) @@ -211,7 +214,12 @@ func newOSFile(routes routes, targetRoot, relPath, absPath string, fi os.FileInf mFile = memfile.New(b) } - of := &osFile{route: route, f: mFile, targetRoot: targetRoot, absPath: absPath, relPath: relPath, size: size} + keyPath := relPath + if cfg.StripIndexHTML { + keyPath = trimIndexHTML(keyPath) + } + + of := &osFile{route: route, f: mFile, targetRoot: targetRoot, absPath: absPath, relPath: relPath, keyPath: keyPath, size: size} if err := of.initContentType(peek); err != nil { return nil, err diff --git a/lib/files_test.go b/lib/files_test.go index 96c2267..067be56 100644 --- a/lib/files_test.go +++ b/lib/files_test.go @@ -91,5 +91,14 @@ func openTestFile(name string) (*osFile, error) { return nil, err } - return newOSFile(nil, "", relPath, absPath, fi) + args := []string{ + "-bucket=mybucket", + } + + cfg, err := ConfigFromArgs(args) + if err != nil { + return nil, err + } + + return newOSFile(cfg, relPath, absPath, fi) } diff --git a/lib/url.go b/lib/url.go index 314088f..06ad771 100644 --- a/lib/url.go +++ b/lib/url.go @@ -1,5 +1,10 @@ package lib +import ( + "path" + "strings" +) + // [RFC 1738](https://www.ietf.org/rfc/rfc1738.txt) // §2.2 func shouldEscape(c byte) bool { @@ -71,3 +76,35 @@ func pathEscapeRFC1738(s string) string { } return string(t) } + +// Like path.Join, but preserves trailing slash.. +func pathJoin(elem ...string) string { + if len(elem) == 0 { + return "" + } + hadSlash := strings.HasSuffix(elem[len(elem)-1], "/") + p := path.Join(elem...) + if hadSlash { + p += "/" + } + return p +} + +// pathClean works like path.Clean but will always preserve a trailing slash. +func pathClean(p string) string { + hadSlash := strings.HasSuffix(p, "/") + p = path.Clean(p) + if hadSlash && !strings.HasSuffix(p, "/") { + p += "/" + } + return p +} + +// trimIndexHTML remaps paths matching "