From 552f9903586ee3e1f5f25921c787fc58cb032a92 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 18 Jul 2020 06:10:16 +0300 Subject: [PATCH] New iris.DirOptions.PushTargetsRegexp rel to: https://github.com/kataras/iris/issues/1562#issuecomment-659702891 Former-commit-id: 778cf9146b730d424040ea9e259ce6500f53b563 --- HISTORY.md | 17 +++++- _examples/file-server/http2push/main.go | 23 ++++--- aliases.go | 8 +++ core/router/fs.go | 80 ++++++++++++++++++++++++- 4 files changed, 116 insertions(+), 12 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 0f538d18..3a8467a5 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -371,7 +371,20 @@ Other Improvements: ![DBUG routes](https://iris-go.com/images/v12.2.0-dbug2.png?v=0) -- Fix [#1562](https://github.com/kataras/iris/issues/1562) +- New `DirOptions.PushTargets` and `PushTargetsRegexp` to push index' assets to the client without additional requests. Inspirated by issue [#1562](https://github.com/kataras/iris/issues/1562). Example matching all `.js, .css and .ico` files (recursively): + +```go +var opts = iris.DirOptions{ + IndexName: "/index.html", + PushTargetsRegexp: map[string]*regexp.Regexp{ + "/": regexp.MustCompile("((.*).js|(.*).css|(.*).ico)$"), + // OR: + // "/": iris.MatchCommonAssets, + }, + Compress: true, +} +``` + - Update jet parser to v4.0.2, closes [#1551](https://github.com/kataras/iris/issues/1551). It contains two breaking changes by its author: - Relative paths on `extends, import, include...` tmpl functions, e.g. `{{extends "../layouts/application.jet"}}` instead of `layouts/application.jet` - the new [jet.Ranger](https://github.com/CloudyKit/jet/pull/165) interface now requires a `ProvidesIndex() bool` method too @@ -380,7 +393,7 @@ Other Improvements: - Fix [#1552](https://github.com/kataras/iris/issues/1552). - Proper listing of root directories on `Party.HandleDir` when its `DirOptions.ShowList` was set to true. - - Customize the file/directory listing page through views, see [example](https://github.com/kataras/iris/tree/master/_examples/file-server/file-server) + - Customize the file/directory listing page through views, see [example](https://github.com/kataras/iris/tree/master/_examples/file-server/file-server). - Socket Sharding as requested at [#1544](https://github.com/kataras/iris/issues/1544). New `iris.WithSocketSharding` Configurator and `SocketSharding bool` setting. diff --git a/_examples/file-server/http2push/main.go b/_examples/file-server/http2push/main.go index 779c7fbc..bbe17320 100644 --- a/_examples/file-server/http2push/main.go +++ b/_examples/file-server/http2push/main.go @@ -1,6 +1,8 @@ package main import ( + "regexp" + "github.com/kataras/iris/v12" ) @@ -13,13 +15,20 @@ var opts = iris.DirOptions{ // // Note: Requires running server under TLS, // that's why we use `iris.TLS` below. - PushTargets: map[string][]string{ - "/": { // Relative path without route prefix. - "favicon.ico", - "js/main.js", - "css/main.css", - // ^ Relative to the index, if need absolute ones start with a slash ('/'). - }, + // PushTargets: map[string][]string{ + // "/": { // Relative path without prefix. + // "favicon.ico", + // "js/main.js", + // "css/main.css", + // // ^ Relative to the index, if need absolute ones start with a slash ('/'). + // }, + // }, + // OR use regexp: + PushTargetsRegexp: map[string]*regexp.Regexp{ + // Match all js, css and ico files + // from all files (recursively). + // "/": regexp.MustCompile("((.*).js|(.*).css|(.*).ico)$"), + "/": iris.MatchCommonAssets, }, Compress: true, ShowList: true, diff --git a/aliases.go b/aliases.go index 1813a426..77f85c6d 100644 --- a/aliases.go +++ b/aliases.go @@ -2,6 +2,7 @@ package iris import ( "net/http" + "regexp" "github.com/kataras/iris/v12/cache" "github.com/kataras/iris/v12/context" @@ -216,6 +217,13 @@ var ( ctx.CompressReader(true) ctx.Next() } + + // MatchCommonAssets is a simple regex expression which + // can be used on `DirOptions.PushTargetsRegexp`. + // It will match and Push + // all available js, css, font and media files. + // Ideal for Single Page Applications. + MatchCommonAssets = regexp.MustCompile("((.*).js|(.*).css|(.*).ico|(.*).png|(.*).ttf|(.*).svg|(.*).webp|(.*).gif)$") ) var ( diff --git a/core/router/fs.go b/core/router/fs.go index 476d9c41..c0b385cb 100644 --- a/core/router/fs.go +++ b/core/router/fs.go @@ -11,6 +11,7 @@ import ( "os" "path" "path/filepath" + "regexp" "sort" "strings" "time" @@ -48,7 +49,23 @@ type DirOptions struct { // be served without additional client's requests (HTTP/2 Push) // when a specific request path (map's key WITHOUT prefix) // is requested and it's not a directory (it's an `IndexFile`). + // + // Example: + // "/": { + // "favicon.ico", + // "js/main.js", + // "css/main.css", + // } PushTargets map[string][]string + // PushTargetsRegexp like `PushTargets` but accepts regexp which + // is compared against all files under a directory (recursively). + // The `IndexName` should be set. + // + // Example: + // "/": regexp.MustCompile("((.*).js|(.*).css|(.*).ico)$") + // See `iris.MatchCommonAssets` too. + PushTargetsRegexp map[string]*regexp.Regexp + // When files should served under compression. Compress bool @@ -407,7 +424,11 @@ func FileServer(directory string, opts ...DirOptions) context.Handler { return } - indexFound := false + var ( + indexFound bool + indexDirectory http.File + ) + // use contents of index.html for directory, if present if info.IsDir() && options.IndexName != "" { // Note that, in contrast of the default net/http mechanism; @@ -425,9 +446,11 @@ func FileServer(directory string, opts ...DirOptions) context.Handler { defer fIndex.Close() infoIndex, err := fIndex.Stat() if err == nil { - info = infoIndex - f = fIndex indexFound = true + indexDirectory = f + + f = fIndex + info = infoIndex } } } @@ -454,6 +477,25 @@ func FileServer(directory string, opts ...DirOptions) context.Handler { } } } + + if regex, ok := options.PushTargetsRegexp[r.URL.Path]; ok { + if pusher, ok := ctx.ResponseWriter().(http.Pusher); ok { + for _, indexAsset := range getFilenamesRecursively(fs, indexDirectory, "") { + // it's an index file, do not pushed that. + if strings.HasSuffix("/"+indexAsset, options.IndexName) { + continue + } + // match using relative path (without the first '/' slash) + // to keep consistency between the `PushTargets` behavior + if regex.MatchString(indexAsset) { + // println("Regex Matched: " + indexAsset) + if err = pusher.Push(path.Join(r.RequestURI, indexAsset), nil); err != nil { + break + } + } + } + } + } } // Still a directory? (we didn't find an index.html file) @@ -532,6 +574,38 @@ func FileServer(directory string, opts ...DirOptions) context.Handler { return h } +func getFilenamesRecursively(fs http.FileSystem, f http.File, parent string) []string { + defer f.Close() + info, err := f.Stat() + if err != nil { + return nil + } + + var filenames []string + + if info.IsDir() { + fileinfos, err := f.Readdir(-1) + if err != nil { + return nil + } + + for _, fileinfo := range fileinfos { + fullname := path.Join(parent, fileinfo.Name()) + ff, err := fs.Open(fullname) + if err != nil { + return nil + } + + filenames = append(filenames, getFilenamesRecursively(fs, ff, fullname)...) + } + + return filenames + } + + filenames = append(filenames, path.Dir(path.Join(parent, info.Name()))) + return filenames +} + // StripPrefix returns a handler that serves HTTP requests // by removing the given prefix from the request URL's Path // and invoking the handler h. StripPrefix handles a