diff --git a/_benchmarks/iris-mvc-templates/main.go b/_benchmarks/iris-mvc-templates/main.go index cfd46558..c743e15a 100644 --- a/_benchmarks/iris-mvc-templates/main.go +++ b/_benchmarks/iris-mvc-templates/main.go @@ -19,7 +19,7 @@ const ( func main() { app := iris.New() app.RegisterView(iris.HTML("./views", ".html").Layout("shared/layout.html")) - app.StaticWeb("/public", publicDir) + app.HandleDir("/public", publicDir) app.OnAnyErrorCode(onError) mvc.New(app).Handle(new(controllers.HomeController)) diff --git a/_examples/README.md b/_examples/README.md index 92408f26..a8f5098c 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -149,7 +149,7 @@ Navigate through examples for a better understanding. - [Write your own custom parameter types](routing/macros/main.go) - [Reverse routing](routing/reverse/main.go) - [Custom Router (high-level)](routing/custom-high-level-router/main.go) -- [Custom Wrapper](routing/custom-wrapper/main.go) +- [Custom Wrapper](routing/custom-wrapper/main.go) **UPDATED** - Custom Context * [method overriding](routing/custom-context/method-overriding/main.go) * [new implementation](routing/custom-context/new-implementation/main.go) @@ -366,14 +366,14 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her ### File Server - [Favicon](file-server/favicon/main.go) -- [Basic](file-server/basic/main.go) -- [Embedding Files Into App Executable File](file-server/embedding-files-into-app/main.go) -- [Embedding Gziped Files Into App Executable File](file-server/embedding-gziped-files-into-app/main.go) +- [Basic](file-server/basic/main.go) **UPDATED** +- [Embedding Files Into App Executable File](file-server/embedding-files-into-app/main.go) **UPDATED** +- [Embedding Gziped Files Into App Executable File](file-server/embedding-gziped-files-into-app/main.go) **UPDATED** - [Send/Force-Download Files](file-server/send-files/main.go) - Single Page Applications - * [single Page Application](file-server/single-page-application/basic/main.go) - * [embedded Single Page Application](file-server/single-page-application/embedded-single-page-application/main.go) - * [embedded Single Page Application with other routes](file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go) + * [single Page Application](file-server/single-page-application/basic/main.go) **UPDATED** + * [embedded Single Page Application](file-server/single-page-application/embedded-single-page-application/main.go) **UPDATED** + * [embedded Single Page Application with other routes](file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go) **UPDATED** ### How to Read from `context.Request() *http.Request` diff --git a/_examples/README_ZH.md b/_examples/README_ZH.md index d8aea7a9..286df607 100644 --- a/_examples/README_ZH.md +++ b/_examples/README_ZH.md @@ -10,7 +10,7 @@ ### 概览 - [Hello world!](hello-world/main.go) -- [Hello WebAssemply!](webassembly/basic/main.go) **NEW** +- [Hello WebAssemply!](webassembly/basic/main.go) - [基础](overview/main.go) - [教程: 在线人数](tutorial/online-visitors/main.go) - [教程: 一个“待完成”MVC Application基于Iris和Vue.js](https://hackernoon.com/a-todo-mvc-application-using-iris-and-vue-js-5019ff870064) @@ -21,7 +21,7 @@ - [教程: DropzoneJS 上传](tutorial/dropzonejs) - [教程: Caddy 服务器使用](tutorial/caddy) - [教程: Iris + MongoDB](https://medium.com/go-language/iris-go-framework-mongodb-552e349eab9c) -- [教程: Apache Kafka的API](tutorial/api-for-apache-kafka) **NEW** +- [教程: Apache Kafka的API](tutorial/api-for-apache-kafka) ### 目录结构 @@ -105,10 +105,10 @@ app.Get("{root:path}", rootWildcardHandler) - [自定义 HTTP 错误](routing/http-errors/main.go) - [动态路径](routing/dynamic-path/main.go) * [根级通配符路径](routing/dynamic-path/root-wildcard/main.go) -- [编写你自己的参数类型](routing/macros/main.go) **NEW** +- [编写你自己的参数类型](routing/macros/main.go) - [反向路由](routing/reverse/main.go) -- [自定义路由(高层级)](routing/custom-high-level-router/main.go) **NEW** -- [自定义包装](routing/custom-wrapper/main.go) +- [自定义路由(高层级)](routing/custom-high-level-router/main.go) +- [自定义包装](routing/custom-wrapper/main.go) **更新** - 自定义上下文    * [方法重写](routing/custom-context/method-overriding/main.go)    * [新实现方式](routing/custom-context/new-implementation/main.go) @@ -121,8 +121,8 @@ app.Get("{root:path}", rootWildcardHandler) - [基础](hero/basic/main.go) - [概览](hero/overview) -- [Sessions](hero/sessions) **NEW** -- [另一种依赖注入的例子和通常的较好实践](hero/smart-contract/main.go) **NEW** +- [Sessions](hero/sessions) +- [另一种依赖注入的例子和通常的较好实践](hero/smart-contract/main.go) **新** ### MVC 模式 @@ -255,14 +255,14 @@ func(c *ExampleController) Get() string | 参考下面的示例 -- [Hello world](mvc/hello-world/main.go) **UPDATED** -- [Session Controller](mvc/session-controller/main.go) **UPDATED** -- [Overview - Plus Repository and Service layers](mvc/overview) **UPDATED** -- [Login showcase - Plus Repository and Service layers](mvc/login) **UPDATED** -- [Singleton](mvc/singleton) **NEW** -- [Websocket Controller](mvc/websocket) **NEW** -- [Register Middleware](mvc/middleware) **NEW** -- [Vue.js Todo MVC](tutorial/vuejs-todo-mvc) **NEW** +- [Hello world](mvc/hello-world/main.go) **更新** +- [Session Controller](mvc/session-controller/main.go) **更新** +- [Overview - Plus Repository and Service layers](mvc/overview) **更新** +- [Login showcase - Plus Repository and Service layers](mvc/login) **更新** +- [Singleton](mvc/singleton) **新** +- [Websocket Controller](mvc/websocket) **新** +- [Register Middleware](mvc/middleware) **新** +- [Vue.js Todo MVC](tutorial/vuejs-todo-mvc) **新** ### 子域名 @@ -316,14 +316,14 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her ### 文件服务器 - [Favicon](file-server/favicon/main.go) -- [基础操作](file-server/basic/main.go) -- [把文件嵌入应用的可执行文件](file-server/embedding-files-into-app/main.go) -- [嵌入Gzip压缩的文件到可咨询文件](file-server/embedding-gziped-files-into-app/main.go) **NEW** +- [基础操作](file-server/basic/main.go) **更新** +- [把文件嵌入应用的可执行文件](file-server/embedding-files-into-app/main.go) **更新** +- [嵌入Gzip压缩的文件到可咨询文件](file-server/embedding-gziped-files-into-app/main.go) **更新** - [上传/(强制)下载文件](file-server/send-files/main.go) - 单页面应用(Single Page Applications) - * [单页面应用](file-server/single-page-application/basic/main.go) - * [嵌入式(embedded)单页面应用](file-server/single-page-application/embedded-single-page-application/main.go) - * [使用额外路由的嵌入式单页面应用](file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go) + * [单页面应用](file-server/single-page-application/basic/main.go) **更新** + * [嵌入式(embedded)单页面应用](file-server/single-page-application/embedded-single-page-application/main.go) **更新** + * [使用额外路由的嵌入式单页面应用](file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go) **更新** ### 如何读取`context.Request() *http.Request` @@ -346,7 +346,7 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her - [写入Gzip压缩](http_responsewriter/write-gzip/main.go) - [流输出Stream Writer](http_responsewriter/stream-writer/main.go) - [数据传递Transactions](http_responsewriter/transactions/main.go) -- [SSE](http_responsewriter/sse/main.go) **NEW** +- [SSE](http_responsewriter/sse/main.go) - [SSE (third-party package usage for server sent events第三方库SSE)](http_responsewriter/sse-third-party/main.go) > The `context/context#ResponseWriter()` returns an enchament version of a http.ResponseWriter, these examples show some places where the Context uses this object. Besides that you can use it as you did before iris. @@ -430,7 +430,7 @@ iris websocket库依赖于它自己的[包](https://github.com/kataras/iris/tree 设计这个包的目的是处理原始websockets,虽然它的API和著名的[socket.io](https://socket.io)很像。我最近读了一片文章,并且对我 决定给iris设计一个**快速的**websocket**限定**包并且不是一个向后传递类socket.io的包。你可以阅读这个链接里的文章https://medium.com/@ivanderbyl/why-you-don-t-need-socket-io-6848f1c871cd。 -- [Basic](websocket/basic) **NEW** +- [Basic](websocket/basic) **新** * [Server](websocket/basic/server.go) * [Go Client](websocket/basic/go-client/client.go) * [Browser Client](websocket/basic/browser/index.html) diff --git a/_examples/cache/simple/main.go b/_examples/cache/simple/main.go index 09f88881..3b3fd88b 100644 --- a/_examples/cache/simple/main.go +++ b/_examples/cache/simple/main.go @@ -74,7 +74,7 @@ func writeMarkdown(ctx iris.Context) { ctx.Markdown(markdownContents) } -/* Note that `StaticWeb` does use the browser's disk caching by-default -therefore, register the cache handler AFTER any StaticWeb calls, +/* Note that `HandleDir` does use the browser's disk caching by-default +therefore, register the cache handler AFTER any HandleDir calls, for a faster solution that server doesn't need to keep track of the response navigate to https://github.com/kataras/iris/blob/master/_examples/cache/client-side/main.go */ diff --git a/_examples/file-server/basic/assets/app2/app22/just_a_text_no_index.txt b/_examples/file-server/basic/assets/app2/app22/just_a_text_no_index.txt new file mode 100644 index 00000000..f1cc1278 --- /dev/null +++ b/_examples/file-server/basic/assets/app2/app22/just_a_text_no_index.txt @@ -0,0 +1 @@ +just a text. \ No newline at end of file diff --git a/_examples/file-server/basic/assets/app2/app2app3/index.html b/_examples/file-server/basic/assets/app2/app2app3/index.html new file mode 100644 index 00000000..750f10ba --- /dev/null +++ b/_examples/file-server/basic/assets/app2/app2app3/index.html @@ -0,0 +1 @@ +

Hello App2App3 index

\ No newline at end of file diff --git a/_examples/file-server/basic/assets/app2/index.html b/_examples/file-server/basic/assets/app2/index.html new file mode 100644 index 00000000..8193d822 --- /dev/null +++ b/_examples/file-server/basic/assets/app2/index.html @@ -0,0 +1 @@ +

Hello App2 index

\ No newline at end of file diff --git a/_examples/file-server/basic/main.go b/_examples/file-server/basic/main.go index 09b4faf8..6ce716e8 100644 --- a/_examples/file-server/basic/main.go +++ b/_examples/file-server/basic/main.go @@ -4,38 +4,48 @@ import ( "github.com/kataras/iris" ) -func main() { +func newApp() *iris.Application { app := iris.New() app.Favicon("./assets/favicon.ico") - // enable gzip, optionally: - // if used before the `StaticXXX` handlers then - // the content byte range feature is gone. - // recommend: turn off for large files especially - // when server has low memory, - // turn on for medium-sized files - // or for large-sized files if they are zipped already, - // i.e "zippedDir/file.gz" - // - // app.Use(iris.Gzip) - // first parameter is the request path // second is the system directory // - // app.StaticWeb("/css", "./assets/css") - // app.StaticWeb("/js", "./assets/js") - // - app.StaticWeb("/static", "./assets") + // app.HandleDir("/css", "./assets/css") + // app.HandleDir("/js", "./assets/js") + app.HandleDir("/static", "./assets", iris.DirOptions{ + // Defaults to "/index.html", if request path is ending with **/*/$IndexName + // then it redirects to **/*(/) which another handler is handling it, + // that another handler, called index handler, is auto-registered by the framework + // if end developer does not managed to handle it by hand. + IndexName: "/index.html", + // When files should served under compression. + Gzip: false, + // List the files inside the current requested directory if `IndexName` not found. + ShowList: false, + // If `ShowList` is true then this function will be used instead of the default one to show the list of files of a current requested directory(dir). + // DirList: func(ctx context.Context, dirName string, dir http.File) error { ... } + // + // Optional validator that loops through each requested resource. + // AssetValidator: func(ctx iris.Context, name string) bool { ... } + }) + + // You can also register any index handler manually, order of registration does not matter: + // app.Get("/static", [...custom middleware...], func(ctx iris.Context) { + // [...custom code...] + // ctx.ServeFile("./assets/index.html", false) + // }) + + // http://localhost:8080/static // http://localhost:8080/static/css/main.css // http://localhost:8080/static/js/jquery-2.1.1.js // http://localhost:8080/static/favicon.ico - app.Run(iris.Addr(":8080")) - - // Note: - // Routing doesn't allows something .StaticWeb("/", "./assets") - // - // To see how you can wrap the router in order to achieve - // wildcard on root path, see "single-page-application". + return app +} + +func main() { + app := newApp() + app.Run(iris.Addr(":8080")) } diff --git a/_examples/file-server/basic/main_test.go b/_examples/file-server/basic/main_test.go new file mode 100644 index 00000000..d0aaef82 --- /dev/null +++ b/_examples/file-server/basic/main_test.go @@ -0,0 +1,93 @@ +package main + +import ( + "io/ioutil" + "path/filepath" + "strings" + "testing" + + "github.com/kataras/iris/httptest" +) + +type resource string + +func (r resource) contentType() string { + switch filepath.Ext(r.String()) { + case ".js": + return "application/javascript" + case ".css": + return "text/css" + case ".ico": + return "image/x-icon" + case ".html", "": + return "text/html" + default: + return "text/plain" + } +} + +func (r resource) String() string { + return string(r) +} + +func (r resource) strip(strip string) string { + s := r.String() + return strings.TrimPrefix(s, strip) +} + +func (r resource) loadFromBase(dir string) string { + filename := r.String() + + filename = r.strip("/static") + if filepath.Ext(filename) == "" { + // root /. + filename = filename + "/index.html" + } + + fullpath := filepath.Join(dir, filename) + + b, err := ioutil.ReadFile(fullpath) + if err != nil { + panic(fullpath + " failed with error: " + err.Error()) + } + + result := string(b) + + return result +} + +func TestFileServerBasic(t *testing.T) { + var urls = []resource{ + "/static/css/main.css", + "/static/js/jquery-2.1.1.js", + "/static/favicon.ico", + "/static/app2", + "/static/app2/app2app3", + "/static", + } + + app := newApp() + // route := app.GetRouteReadOnly("GET/{file:path}") + // if route == nil { + // app.Logger().Fatalf("expected a route to serve files") + // } + + // if expected, got := "./assets", route.StaticDir(); expected != got { + // app.Logger().Fatalf("expected route's static directory to be: '%s' but got: '%s'", expected, got) + // } + + // if !route.StaticDirContainsIndex() { + // app.Logger().Fatalf("epxected ./assets to contain an %s file", "/index.html") + // } + + e := httptest.New(t, app) + for _, u := range urls { + url := u.String() + contents := u.loadFromBase("./assets") + + e.GET(url).Expect(). + Status(httptest.StatusOK). + ContentType(u.contentType(), app.ConfigurationReadOnly().GetCharset()). + Body().Equal(contents) + } +} diff --git a/_examples/file-server/embedding-files-into-app/main.go b/_examples/file-server/embedding-files-into-app/main.go index ac43c3a8..17f94f03 100644 --- a/_examples/file-server/embedding-files-into-app/main.go +++ b/_examples/file-server/embedding-files-into-app/main.go @@ -14,8 +14,14 @@ import ( // See `file-server/embedding-gziped-files-into-app` example as well. func newApp() *iris.Application { app := iris.New() + app.Logger().SetLevel("debug") - app.StaticEmbedded("/static", "./assets", Asset, AssetNames) + app.HandleDir("/static", "./assets", iris.DirOptions{ + Asset: Asset, + AssetInfo: AssetInfo, + AssetNames: AssetNames, + ShowList: true, + }) return app } diff --git a/_examples/file-server/embedding-files-into-app/main_test.go b/_examples/file-server/embedding-files-into-app/main_test.go index f1d5a41a..24421679 100644 --- a/_examples/file-server/embedding-files-into-app/main_test.go +++ b/_examples/file-server/embedding-files-into-app/main_test.go @@ -66,12 +66,21 @@ var urls = []resource{ } // if bindata's values matches with the assets/... contents -// and secondly if the StaticEmbedded had successfully registered +// and secondly if the HandleDir had successfully registered // the routes and gave the correct response. func TestEmbeddingFilesIntoApp(t *testing.T) { app := newApp() e := httptest.New(t, app) + route := app.GetRouteReadOnly("GET/static/{file:path}") + if route == nil { + t.Fatalf("expected a route to serve embedded files") + } + + if len(route.StaticSites()) > 0 { + t.Fatalf("not expected a static site, the ./assets directory or its subdirectories do not contain any index.html") + } + if runtime.GOOS != "windows" { // remove the embedded static favicon for !windows, // it should be built for unix-specific in order to be work diff --git a/_examples/file-server/embedding-gziped-files-into-app/main.go b/_examples/file-server/embedding-gziped-files-into-app/main.go index 944c251d..75640faf 100644 --- a/_examples/file-server/embedding-gziped-files-into-app/main.go +++ b/_examples/file-server/embedding-gziped-files-into-app/main.go @@ -16,12 +16,13 @@ import ( func newApp() *iris.Application { app := iris.New() - // Note the `GzipAsset` and `GzipAssetNames` are different from `go-bindata`'s `Asset` and `AssetNames, - // that means that you can use both `go-bindata` and `bindata` tools, - // the `go-bindata` can be used for the view engine's `Binary` method - // and the `bindata` with the `StaticEmbeddedGzip` (x8 times faster than the StaticEmbeded with `go-bindata`). - app.StaticEmbeddedGzip("/static", "./assets", GzipAsset, GzipAssetNames) - + // Note the `GzipAsset` and `GzipAssetNames` are different from `go-bindata`'s `Asset`, + // do not set the `Gzip` option to true, it's already managed by the kataras/bindata. + app.HandleDir("/static", "./assets", iris.DirOptions{ + Asset: GzipAsset, + AssetInfo: GzipAssetInfo, + AssetNames: GzipAssetNames, + }) return app } diff --git a/_examples/file-server/embedding-gziped-files-into-app/main_test.go b/_examples/file-server/embedding-gziped-files-into-app/main_test.go index c5b4799b..4518bfdf 100644 --- a/_examples/file-server/embedding-gziped-files-into-app/main_test.go +++ b/_examples/file-server/embedding-gziped-files-into-app/main_test.go @@ -67,7 +67,7 @@ var urls = []resource{ } // if bindata's values matches with the assets/... contents -// and secondly if the StaticEmbedded had successfully registered +// and secondly if the HandleDir had successfully registered // the routes and gave the correct response. func TestEmbeddingGzipFilesIntoApp(t *testing.T) { app := newApp() diff --git a/_examples/file-server/single-page-application/basic/main.go b/_examples/file-server/single-page-application/basic/main.go index 649f7066..fc173bcd 100644 --- a/_examples/file-server/single-page-application/basic/main.go +++ b/_examples/file-server/single-page-application/basic/main.go @@ -20,15 +20,7 @@ func newApp() *iris.Application { ctx.View("index.html") }) - // or just serve index.html as it is: - // app.Get("/{f:path}", func(ctx iris.Context) { - // ctx.ServeFile("index.html", false) - // }) - - assetHandler := app.StaticHandler("./public", false, false) - // as an alternative of SPA you can take a look at the /routing/dynamic-path/root-wildcard - // example too - app.SPA(assetHandler) + app.HandleDir("/", "./public") return app } diff --git a/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go b/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go index 14140f92..6c3522aa 100644 --- a/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go +++ b/_examples/file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go @@ -9,11 +9,21 @@ import "github.com/kataras/iris" func newApp() *iris.Application { app := iris.New() - app.OnErrorCode(404, func(ctx iris.Context) { + app.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) { ctx.Writef("404 not found here") }) - app.StaticEmbedded("/", "./public", Asset, AssetNames) + app.HandleDir("/", "./public", iris.DirOptions{ + Asset: Asset, + AssetInfo: AssetInfo, + AssetNames: AssetNames, + // IndexName: "index.html", // default. + // If you want to show a list of embedded files when inside a directory without an index file: + // ShowList: true, + // DirList: func(ctx iris.Context, dirName string, f http.File) error { + // // [Optional, custom code to show the html list]. + // } + }) // Note: // if you want a dynamic index page then see the file-server/embedded-single-page-application diff --git a/_examples/file-server/single-page-application/embedded-single-page-application/main.go b/_examples/file-server/single-page-application/embedded-single-page-application/main.go index 8c44a3e0..e0b0570a 100644 --- a/_examples/file-server/single-page-application/embedded-single-page-application/main.go +++ b/_examples/file-server/single-page-application/embedded-single-page-application/main.go @@ -22,16 +22,11 @@ func newApp() *iris.Application { ctx.View("index.html") }) - assetHandler := iris.StaticEmbeddedHandler("./public", Asset, AssetNames, false) // keep that false if you use the `go-bindata` tool. - // as an alternative of SPA you can take a look at the /routing/dynamic-path/root-wildcard - // example too - // or - // app.StaticEmbedded if you don't want to redirect on index.html and simple serve your SPA app (recommended). - - // public/index.html is a dynamic view, it's handlded by root, - // and we don't want to be visible as a raw data, so we will - // the return value of `app.SPA` to modify the `IndexNames` by; - app.SPA(assetHandler).AddIndexName("index.html") + app.HandleDir("/", "./public", iris.DirOptions{ + Asset: Asset, + AssetInfo: AssetInfo, + AssetNames: AssetNames, + }) return app } @@ -45,11 +40,3 @@ func main() { // http://localhost:8080/css/main.css app.Run(iris.Addr(":8080")) } - -// Note that app.Use/UseGlobal/Done will be executed -// only to the registered routes like our index (app.Get("/", ..)). -// The file server is clean, but you can still add middleware to that by wrapping its "assetHandler". -// -// With this method, unlike StaticWeb("/" , "./public") which is not working by-design anymore, -// all custom http errors and all routes are working fine with a file server that is registered -// to the root path of the server. diff --git a/_examples/http_request/request-logger/request-logger-file-json/main.go b/_examples/http_request/request-logger/request-logger-file-json/main.go index 2387de48..9fbe24c6 100644 --- a/_examples/http_request/request-logger/request-logger-file-json/main.go +++ b/_examples/http_request/request-logger/request-logger-file-json/main.go @@ -2,19 +2,56 @@ package main import ( "fmt" + "io" "os" - "runtime" "strings" "time" "github.com/kataras/iris" "github.com/kataras/iris/middleware/logger" - - "github.com/kataras/golog" ) const deleteFileOnExit = false +func newRequestLogger(newWriter io.Writer) iris.Handler { + c := logger.Config{} + + // we don't want to use the logger + // to log requests to assets and etc + c.AddSkipper(func(ctx iris.Context) bool { + path := ctx.Path() + for _, ext := range excludeExtensions { + if strings.HasSuffix(path, ext) { + return true + } + } + return false + }) + + c.LogFuncCtx = func(ctx iris.Context, latency time.Duration) { + datetime := time.Now().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()) + customHandlerMessage := ctx.Values().GetString("log_message") + + file, line := ctx.HandlerFileLine() + source := fmt.Sprintf("%s:%d", file, line) + + // this will just append a line without an array of javascript objects, readers of this file should read one line per log javascript object, + // however, you can improve it even more, this is just a simple example on how to use the `LogFuncCtx`. + jsonStr := fmt.Sprintf(`{"datetime":"%s","level":"%s","source":"%s","latency": "%s","status": %d,"method":"%s","path":"%s","message":"%s"}`, + datetime, "INFO", source, latency.String(), ctx.GetStatusCode(), ctx.Method(), ctx.Path(), customHandlerMessage) + + fmt.Fprintln(newWriter, jsonStr) + } + + return logger.New(c) +} + +func h(ctx iris.Context) { + ctx.Values().Set("log_message", "something to give more info to the request logger") + + ctx.Writef("Hello from %s", ctx.Path()) +} + func main() { app := iris.New() @@ -26,56 +63,21 @@ func main() { } }() - // Handle the logs by yourself using the `app.Logger#Handle` method. - // Return true if that handled, otherwise will print to the screen. - // You can also use the `app.Logger#SetOutput/AddOutput` to change or add - // multi (io.Writer) outputs if you just want to print the message - // somewhere else than the terminal screen. - app.Logger().Handle(func(l *golog.Log) bool { - _, fn, line, _ := runtime.Caller(5) - - var ( - // formatted date string based on the `golog#TimeFormat`, which can be customized. - // Or use the golog.Log#Time field to get the exact time.Time instance. - datetime = l.FormatTime() - // the log's message level. - level = golog.GetTextForLevel(l.Level, false) - // the log's message. - message = l.Message - // the source code line of where it is called, - // this can differ on your app, see runtime.Caller(%d). - source = fmt.Sprintf("%s#%d", fn, line) - ) - - // You can always use a custom json structure and json.Marshal and logFile.Write(its result) - // but it is faster to just build your JSON string by yourself as we do below. - jsonStr := fmt.Sprintf(`{"datetime":"%s","level":"%s","message":"%s","source":"%s"}`, datetime, level, message, source) - fmt.Fprintln(logFile, jsonStr) - - /* Example output: - {"datetime":"2018/10/31 13:13","level":"[INFO]","message":"My server started","source":"c:/mygopath/src/github.com/kataras/iris/_examples/http_request/request-logger/request-logger-file-json/main.go#71"} - */ - return true - }) - - r := newRequestLogger() + r := newRequestLogger(logFile) app.Use(r) app.OnAnyErrorCode(r, func(ctx iris.Context) { ctx.HTML("

Error: Please try this instead.

") }) - h := func(ctx iris.Context) { - ctx.Writef("Hello from %s", ctx.Path()) - } - app.Get("/", h) app.Get("/1", h) app.Get("/2", h) - app.Logger().Info("My server started") + app.Get("/", h) + // http://localhost:8080 // http://localhost:8080/1 // http://localhost:8080/2 @@ -92,29 +94,6 @@ var excludeExtensions = [...]string{ ".svg", } -func newRequestLogger() iris.Handler { - c := logger.Config{ - Status: true, - IP: true, - Method: true, - Path: true, - } - - // we don't want to use the logger - // to log requests to assets and etc - c.AddSkipper(func(ctx iris.Context) bool { - path := ctx.Path() - for _, ext := range excludeExtensions { - if strings.HasSuffix(path, ext) { - return true - } - } - return false - }) - - return logger.New(c) -} - // get a filename based on the date, file logs works that way the most times // but these are just a sugar. func todayFilename() string { diff --git a/_examples/http_request/request-logger/request-logger-file/main.go b/_examples/http_request/request-logger/request-logger-file/main.go index dc37f8a1..1fbe5143 100644 --- a/_examples/http_request/request-logger/request-logger-file/main.go +++ b/_examples/http_request/request-logger/request-logger-file/main.go @@ -85,10 +85,10 @@ func newRequestLogger() (h iris.Handler, close func() error) { return err } - c.LogFunc = func(now time.Time, latency time.Duration, status, ip, method, path string, message interface{}, headerMessage interface{}) { - output := logger.Columnize(now.Format("2006/01/02 - 15:04:05"), latency, status, ip, method, path, message, headerMessage) + c.LogFunc = func(endTime time.Time, latency time.Duration, status, ip, method, path string, message interface{}, headerMessage interface{}) { + output := logger.Columnize(endTime.Format("2006/01/02 - 15:04:05"), latency, status, ip, method, path, message, headerMessage) logFile.Write([]byte(output)) - } + } // or make use of the `LogFuncCtx`, see the '../request-logger-file-json' example for more. // we don't want to use the logger // to log requests to assets and etc diff --git a/_examples/mvc/login/main.go b/_examples/mvc/login/main.go index 3d77e08a..e1824b8f 100644 --- a/_examples/mvc/login/main.go +++ b/_examples/mvc/login/main.go @@ -28,7 +28,7 @@ func main() { Reload(true) app.RegisterView(tmpl) - app.StaticWeb("/public", "./web/public") + app.HandleDir("/public", "./web/public") app.OnAnyErrorCode(func(ctx iris.Context) { ctx.ViewData("Message", ctx.Values(). diff --git a/_examples/routing/README.md b/_examples/routing/README.md index 88c40db9..b776376e 100644 --- a/_examples/routing/README.md +++ b/_examples/routing/README.md @@ -559,89 +559,72 @@ Example Code: package main import ( - "net/http" - "strings" + "net/http" + "strings" - "github.com/kataras/iris" + "github.com/kataras/iris" ) // In this example you'll just see one use case of .WrapRouter. // You can use the .WrapRouter to add custom logic when or when not the router should // be executed in order to execute the registered routes' handlers. -// -// To see how you can serve files on root "/" without a custom wrapper -// just navigate to the "file-server/single-page-application" example. -// -// This is just for the proof of concept, you can skip this tutorial if it's too much for you. +func newApp() *iris.Application { + app := iris.New() + + app.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) { + ctx.HTML("Resource Not found") + }) + + app.Get("/profile/{username}", func(ctx iris.Context) { + ctx.Writef("Hello %s", ctx.Params().Get("username")) + }) + + app.HandleDir("/", "./public") + + myOtherHandler := func(ctx iris.Context) { + ctx.Writef("inside a handler which is fired manually by our custom router wrapper") + } + + // wrap the router with a native net/http handler. + // if url does not contain any "." (i.e: .css, .js...) + // (depends on the app , you may need to add more file-server exceptions), + // then the handler will execute the router that is responsible for the + // registered routes (look "/" and "/profile/{username}") + // if not then it will serve the files based on the root "/" path. + app.WrapRouter(func(w http.ResponseWriter, r *http.Request, router http.HandlerFunc) { + path := r.URL.Path + + if strings.HasPrefix(path, "/other") { + // acquire and release a context in order to use it to execute + // our custom handler + // remember: we use net/http.Handler because here we are in the "low-level", before the router itself. + ctx := app.ContextPool.Acquire(w, r) + myOtherHandler(ctx) + app.ContextPool.Release(ctx) + return + } + + router.ServeHTTP(w, r) // else continue serving routes as usual. + }) + + return app +} func main() { - app := iris.New() + app := newApp() - app.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) { - ctx.HTML("Resource Not found") - }) + // http://localhost:8080 + // http://localhost:8080/index.html + // http://localhost:8080/app.js + // http://localhost:8080/css/main.css + // http://localhost:8080/profile/anyusername + // http://localhost:8080/other/random + app.Run(iris.Addr(":8080")) - app.Get("/", func(ctx iris.Context) { - ctx.ServeFile("./public/index.html", false) - }) - - app.Get("/profile/{username}", func(ctx iris.Context) { - ctx.Writef("Hello %s", ctx.Params().Get("username")) - }) - - // serve files from the root "/", if we used .StaticWeb it could override - // all the routes because of the underline need of wildcard. - // Here we will see how you can by-pass this behavior - // by creating a new file server handler and - // setting up a wrapper for the router(like a "low-level" middleware) - // in order to manually check if we want to process with the router as normally - // or execute the file server handler instead. - - // use of the .StaticHandler - // which is the same as StaticWeb but it doesn't - // registers the route, it just returns the handler. - fileServer := app.StaticHandler("./public", false, false) - - // wrap the router with a native net/http handler. - // if url does not contain any "." (i.e: .css, .js...) - // (depends on the app , you may need to add more file-server exceptions), - // then the handler will execute the router that is responsible for the - // registered routes (look "/" and "/profile/{username}") - // if not then it will serve the files based on the root "/" path. - app.WrapRouter(func(w http.ResponseWriter, r *http.Request, router http.HandlerFunc) { - path := r.URL.Path - // Note that if path has suffix of "index.html" it will auto-permant redirect to the "/", - // so our first handler will be executed instead. - - if !strings.Contains(path, ".") { - // if it's not a resource then continue to the router as normally. <-- IMPORTANT - router(w, r) - return - } - // acquire and release a context in order to use it to execute - // our file server - // remember: we use net/http.Handler because here we are in the "low-level", before the router itself. - ctx := app.ContextPool.Acquire(w, r) - fileServer(ctx) - app.ContextPool.Release(ctx) - }) - - // http://localhost:8080 - // http://localhost:8080/index.html - // http://localhost:8080/app.js - // http://localhost:8080/css/main.css - // http://localhost:8080/profile/anyusername - app.Run(iris.Addr(":8080")) - - // Note: In this example we just saw one use case, - // you may want to .WrapRouter or .Downgrade in order to bypass the iris' default router, i.e: - // you can use that method to setup custom proxies too. - // - // If you just want to serve static files on other path than root - // you can just use the StaticWeb, i.e: - // .StaticWeb("/static", "./public") - // ________________________________requestPath, systemPath + // Note: In this example we just saw one use case, + // you may want to .WrapRouter or .Downgrade in order to bypass the iris' default router, i.e: + // you can use that method to setup custom proxies too. } ``` @@ -1355,7 +1338,7 @@ type Context interface { // You can define your own "Content-Type" with `context#ContentType`, before this function call. // // This function doesn't support resuming (by range), - // use ctx.SendFile or router's `StaticWeb` instead. + // use ctx.SendFile or router's `HandleDir` instead. ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error // ServeFile serves a file (to send a file, a zip for example to the client you should use the `SendFile` instead) // receives two parameters @@ -1365,7 +1348,7 @@ type Context interface { // You can define your own "Content-Type" with `context#ContentType`, before this function call. // // This function doesn't support resuming (by range), - // use ctx.SendFile or router's `StaticWeb` instead. + // use ctx.SendFile or router's `HandleDir` instead. // // Use it when you want to serve dynamic files to the client. ServeFile(filename string, gzipCompression bool) error diff --git a/_examples/routing/custom-wrapper/main.go b/_examples/routing/custom-wrapper/main.go index dfcd0402..ea8bfae4 100644 --- a/_examples/routing/custom-wrapper/main.go +++ b/_examples/routing/custom-wrapper/main.go @@ -10,11 +10,6 @@ import ( // In this example you'll just see one use case of .WrapRouter. // You can use the .WrapRouter to add custom logic when or when not the router should // be executed in order to execute the registered routes' handlers. -// -// To see how you can serve files on root "/" without a custom wrapper -// just navigate to the "file-server/single-page-application" example. -// -// This is just for the proof of concept, you can skip this tutorial if it's too much for you. func newApp() *iris.Application { app := iris.New() @@ -23,26 +18,15 @@ func newApp() *iris.Application { ctx.HTML("Resource Not found") }) - app.Get("/", func(ctx iris.Context) { - ctx.ServeFile("./public/index.html", false) - }) - app.Get("/profile/{username}", func(ctx iris.Context) { ctx.Writef("Hello %s", ctx.Params().Get("username")) }) - // serve files from the root "/", if we used .StaticWeb it could override - // all the routes because of the underline need of wildcard. - // Here we will see how you can by-pass this behavior - // by creating a new file server handler and - // setting up a wrapper for the router(like a "low-level" middleware) - // in order to manually check if we want to process with the router as normally - // or execute the file server handler instead. + app.HandleDir("/", "./public") - // use of the .StaticHandler - // which is the same as StaticWeb but it doesn't - // registers the route, it just returns the handler. - fileServer := app.StaticHandler("./public", false, false) + myOtherHandler := func(ctx iris.Context) { + ctx.Writef("inside a handler which is fired manually by our custom router wrapper") + } // wrap the router with a native net/http handler. // if url does not contain any "." (i.e: .css, .js...) @@ -52,19 +36,18 @@ func newApp() *iris.Application { // if not then it will serve the files based on the root "/" path. app.WrapRouter(func(w http.ResponseWriter, r *http.Request, router http.HandlerFunc) { path := r.URL.Path - // Note that if path has suffix of "index.html" it will auto-permant redirect to the "/", - // so our first handler will be executed instead. - if !strings.Contains(path, ".") { // if it's not a resource then continue to the router as normally. - router(w, r) + if strings.HasPrefix(path, "/other") { + // acquire and release a context in order to use it to execute + // our custom handler + // remember: we use net/http.Handler because here we are in the "low-level", before the router itself. + ctx := app.ContextPool.Acquire(w, r) + myOtherHandler(ctx) + app.ContextPool.Release(ctx) return } - // acquire and release a context in order to use it to execute - // our file server - // remember: we use net/http.Handler because here we are in the "low-level", before the router itself. - ctx := app.ContextPool.Acquire(w, r) - fileServer(ctx) - app.ContextPool.Release(ctx) + + router.ServeHTTP(w, r) // else continue serving routes as usual. }) return app @@ -78,14 +61,10 @@ func main() { // http://localhost:8080/app.js // http://localhost:8080/css/main.css // http://localhost:8080/profile/anyusername + // http://localhost:8080/other/random app.Run(iris.Addr(":8080")) // Note: In this example we just saw one use case, // you may want to .WrapRouter or .Downgrade in order to bypass the iris' default router, i.e: // you can use that method to setup custom proxies too. - // - // If you just want to serve static files on other path than root - // you can just use the StaticWeb, i.e: - // .StaticWeb("/static", "./public") - // ________________________________requestPath, systemPath } diff --git a/_examples/routing/custom-wrapper/main_test.go b/_examples/routing/custom-wrapper/main_test.go index 74d36bdf..5e45a86d 100644 --- a/_examples/routing/custom-wrapper/main_test.go +++ b/_examples/routing/custom-wrapper/main_test.go @@ -56,4 +56,6 @@ func TestCustomWrapper(t *testing.T) { Status(httptest.StatusOK). Body().Equal(contents) } + + e.GET("/other/something").Expect().Status(httptest.StatusOK) } diff --git a/_examples/routing/custom-wrapper/public/index.html b/_examples/routing/custom-wrapper/public/index.html index c52c1613..960869d7 100644 --- a/_examples/routing/custom-wrapper/public/index.html +++ b/_examples/routing/custom-wrapper/public/index.html @@ -1,7 +1,7 @@ - {{ .Page.Title }} + Index Page diff --git a/_examples/routing/overview/main.go b/_examples/routing/overview/main.go index d2ff7178..253daa98 100644 --- a/_examples/routing/overview/main.go +++ b/_examples/routing/overview/main.go @@ -35,17 +35,17 @@ func main() { // maps to ./public/assets/css/bootstrap.min.css file at system location. // GET: http://localhost:8080/assets/js/react.min.js // maps to ./public/assets/js/react.min.js file at system location. - app.StaticWeb("/assets", "./public/assets") + app.HandleDir("/assets", "./public/assets") /* OR // GET: http://localhost:8080/js/react.min.js // maps to ./public/assets/js/react.min.js file at system location. - app.StaticWeb("/js", "./public/assets/js") + app.HandleDir("/js", "./public/assets/js") // GET: http://localhost:8080/css/bootstrap.min.css // maps to ./public/assets/css/bootstrap.min.css file at system location. - app.StaticWeb("/css", "./public/assets/css") + app.HandleDir("/css", "./public/assets/css") */ diff --git a/_examples/structuring/bootstrap/bootstrap/bootstrapper.go b/_examples/structuring/bootstrap/bootstrap/bootstrapper.go index 9efa7df6..f8b94942 100644 --- a/_examples/structuring/bootstrap/bootstrap/bootstrapper.go +++ b/_examples/structuring/bootstrap/bootstrap/bootstrapper.go @@ -112,7 +112,7 @@ func (b *Bootstrapper) Bootstrap() *Bootstrapper { // static files b.Favicon(StaticAssets + Favicon) - b.StaticWeb(StaticAssets[1:len(StaticAssets)-1], StaticAssets) + b.HandleDir(StaticAssets[1:len(StaticAssets)-1], StaticAssets) // middleware, after static files b.Use(recover.New()) diff --git a/_examples/structuring/login-mvc-single-responsibility-package/main.go b/_examples/structuring/login-mvc-single-responsibility-package/main.go index 9f5e0911..e6232904 100644 --- a/_examples/structuring/login-mvc-single-responsibility-package/main.go +++ b/_examples/structuring/login-mvc-single-responsibility-package/main.go @@ -18,7 +18,7 @@ func main() { app.RegisterView(iris.HTML("./views", ".html").Layout("shared/layout.html")) - app.StaticWeb("/public", "./public") + app.HandleDir("/public", "./public") mvc.Configure(app, configureMVC) diff --git a/_examples/subdomains/multi/main.go b/_examples/subdomains/multi/main.go index 5878f5f3..7c0627d9 100644 --- a/_examples/subdomains/multi/main.go +++ b/_examples/subdomains/multi/main.go @@ -11,8 +11,8 @@ func main() { * Setup static files */ - app.StaticWeb("/assets", "./public/assets") - app.StaticWeb("/upload_resources", "./public/upload_resources") + app.HandleDir("/assets", "./public/assets") + app.HandleDir("/upload_resources", "./public/upload_resources") dashboard := app.Party("dashboard.") { diff --git a/_examples/subdomains/www/main_test.go b/_examples/subdomains/www/main_test.go index db6c9421..ffd756cc 100644 --- a/_examples/subdomains/www/main_test.go +++ b/_examples/subdomains/www/main_test.go @@ -41,7 +41,7 @@ func TestSubdomainWWW(t *testing.T) { } host := "localhost:1111" - e := httptest.New(t, app, httptest.URL("http://"+host), httptest.Debug(false)) + e := httptest.New(t, app, httptest.Debug(false)) for _, test := range tests { diff --git a/_examples/tutorial/dropzonejs/README.md b/_examples/tutorial/dropzonejs/README.md index 9c8bbab6..b3380888 100644 --- a/_examples/tutorial/dropzonejs/README.md +++ b/_examples/tutorial/dropzonejs/README.md @@ -101,7 +101,7 @@ func main() { app.RegisterView(iris.HTML("./views", ".html")) // Make the /public route path to statically serve the ./public/... contents - app.StaticWeb("/public", "./public") + app.HandleDir("/public", "./public") // Render the actual form // GET: http://localhost:8080 diff --git a/_examples/tutorial/dropzonejs/README_PART2.md b/_examples/tutorial/dropzonejs/README_PART2.md index 331e0f19..536ea41e 100644 --- a/_examples/tutorial/dropzonejs/README_PART2.md +++ b/_examples/tutorial/dropzonejs/README_PART2.md @@ -168,7 +168,7 @@ func main() { app := iris.New() app.RegisterView(iris.HTML("./views", ".html")) - app.StaticWeb("/public", "./public") + app.HandleDir("/public", "./public") app.Get("/", func(ctx iris.Context) { ctx.View("upload.html") diff --git a/_examples/tutorial/dropzonejs/src/main.go b/_examples/tutorial/dropzonejs/src/main.go index 86e54982..c6cc67ad 100644 --- a/_examples/tutorial/dropzonejs/src/main.go +++ b/_examples/tutorial/dropzonejs/src/main.go @@ -124,7 +124,7 @@ func main() { app := iris.New() app.RegisterView(iris.HTML("./views", ".html")) - app.StaticWeb("/public", "./public") + app.HandleDir("/public", "./public") app.Get("/", func(ctx iris.Context) { ctx.View("upload.html") diff --git a/_examples/tutorial/mongodb/README.md b/_examples/tutorial/mongodb/README.md index 644a93d0..967eb118 100644 --- a/_examples/tutorial/mongodb/README.md +++ b/_examples/tutorial/mongodb/README.md @@ -8,7 +8,7 @@ Article is coming soon, follow and stay tuned Read [the fully functional example](main.go). ```sh -$ go get -u github.com/mongodb/mongo-go-driver +$ go get -u go.mongodb.org/mongo-driver/... $ go get -u github.com/joho/godotenv ``` diff --git a/_examples/tutorial/mongodb/main.go b/_examples/tutorial/mongodb/main.go index 56702ebd..9ba3c191 100644 --- a/_examples/tutorial/mongodb/main.go +++ b/_examples/tutorial/mongodb/main.go @@ -1,6 +1,6 @@ package main -// go get -u github.com/mongodb/mongo-go-driver +// go get -u go.mongodb.org/mongo-driver // go get -u github.com/joho/godotenv import ( @@ -19,7 +19,7 @@ import ( "github.com/kataras/iris" - "github.com/mongodb/mongo-go-driver/mongo" + "go.mongodb.org/mongo-driver/mongo" ) const version = "0.0.1" diff --git a/_examples/tutorial/mongodb/store/movie.go b/_examples/tutorial/mongodb/store/movie.go index 3b995e47..133cddc9 100644 --- a/_examples/tutorial/mongodb/store/movie.go +++ b/_examples/tutorial/mongodb/store/movie.go @@ -4,11 +4,11 @@ import ( "context" "errors" - "github.com/mongodb/mongo-go-driver/bson" - "github.com/mongodb/mongo-go-driver/bson/primitive" - "github.com/mongodb/mongo-go-driver/mongo" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" // up to you: - // "github.com/mongodb/mongo-go-driver/mongo/options" + // "go.mongodb.org/mongo-driver/mongo/options" ) type Movie struct { diff --git a/_examples/tutorial/online-visitors/main.go b/_examples/tutorial/online-visitors/main.go index e6f3fa11..968ad284 100644 --- a/_examples/tutorial/online-visitors/main.go +++ b/_examples/tutorial/online-visitors/main.go @@ -23,7 +23,7 @@ func main() { app.Any("/iris-ws.js", websocket.ClientHandler()) // register static assets request path and system directory - app.StaticWeb("/js", "./static/assets/js") + app.HandleDir("/js", "./static/assets/js") h := func(ctx iris.Context) { ctx.ViewData("", page{PageID: "index page"}) diff --git a/_examples/tutorial/url-shortener/main.go b/_examples/tutorial/url-shortener/main.go index 1771f766..7e9e488b 100644 --- a/_examples/tutorial/url-shortener/main.go +++ b/_examples/tutorial/url-shortener/main.go @@ -49,7 +49,7 @@ func newApp(db *DB) *iris.Application { app.RegisterView(tmpl) // Serve static files (css) - app.StaticWeb("/static", "./resources") + app.HandleDir("/static", "./resources") indexHandler := func(ctx iris.Context) { ctx.ViewData("URL_COUNT", db.Len()) diff --git a/_examples/tutorial/vuejs-todo-mvc/README.md b/_examples/tutorial/vuejs-todo-mvc/README.md index 9e0e582a..d46b7a80 100644 --- a/_examples/tutorial/vuejs-todo-mvc/README.md +++ b/_examples/tutorial/vuejs-todo-mvc/README.md @@ -500,7 +500,7 @@ func main() { // no need for any server-side template here, // actually if you're going to just use vue without any // back-end services, you can just stop afer this line and start the server. - app.StaticWeb("/", "./public") + app.HandleDir("/", "./public") // configure the http sessions. sess := sessions.New(sessions.Config{ diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/main.go b/_examples/tutorial/vuejs-todo-mvc/src/web/main.go index 34e5ed88..d1c3815b 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/web/main.go +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/main.go @@ -19,7 +19,7 @@ func main() { // no need for any server-side template here, // actually if you're going to just use vue without any // back-end services, you can just stop afer this line and start the server. - app.StaticWeb("/", "./public") + app.HandleDir("/", "./public") // configure the http sessions. sess := sessions.New(sessions.Config{ diff --git a/_examples/view/template_django_0/main.go b/_examples/view/template_django_0/main.go new file mode 100644 index 00000000..f1bc8957 --- /dev/null +++ b/_examples/view/template_django_0/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "time" + + "github.com/kataras/iris" + + // optionally, registers filters like `timesince`. + _ "github.com/flosch/pongo2-addons" +) + +var startTime = time.Now() + +func main() { + app := iris.New() + + tmpl := iris.Django("./templates", ".html") + tmpl.Reload(true) // reload templates on each request (development mode) + tmpl.AddFunc("greet", func(s string) string { // {{greet(name)}} + return "Greetings " + s + "!" + }) + + // tmpl.RegisterFilter("myFilter", myFilter) // {{"simple input for filter"|myFilter}} + app.RegisterView(tmpl) + + app.Get("/", hi) + + // http://localhost:8080 + app.Run(iris.Addr(":8080"), iris.WithCharset("UTF-8")) // defaults to that but you can change it. +} + +func hi(ctx iris.Context) { + // ctx.ViewData("title", "Hi Page") + // ctx.ViewData("name", "iris") + // ctx.ViewData("serverStartTime", startTime) + // or if you set all view data in the same handler you can use the + // iris.Map/pongo2.Context/map[string]interface{}, look below: + + ctx.View("hi.html", iris.Map{ + "title": "Hi Page", + "name": "iris", + "serverStartTime": startTime, + }) +} diff --git a/_examples/view/template_django_0/templates/hi.html b/_examples/view/template_django_0/templates/hi.html new file mode 100644 index 00000000..945782e4 --- /dev/null +++ b/_examples/view/template_django_0/templates/hi.html @@ -0,0 +1,12 @@ + + +{{title}} + + +

Hi {{name|capfirst}}

+ +

{{greet(name)}}

+ +

Server started about {{serverStartTime|timesince}}. Refresh the page to see different result

+ + diff --git a/_examples/webassembly/basic/main.go b/_examples/webassembly/basic/main.go index 4099da53..9bf19515 100644 --- a/_examples/webassembly/basic/main.go +++ b/_examples/webassembly/basic/main.go @@ -14,7 +14,7 @@ func main() { // we could serve your assets like this the shake of the example, // never include the .go files there in production. - app.StaticWeb("/", "./client") + app.HandleDir("/", "./client") app.Get("/", func(ctx iris.Context) { ctx.ServeFile("./client/hello.html", false) // true for gzip. diff --git a/_examples/websocket/basic/server.go b/_examples/websocket/basic/server.go index be101e45..e030fe9b 100644 --- a/_examples/websocket/basic/server.go +++ b/_examples/websocket/basic/server.go @@ -52,7 +52,7 @@ func main() { }) // serves the npm browser websocket client usage example. - app.StaticWeb("/browserify", "./browserify") + app.HandleDir("/browserify", "./browserify") app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed)) } diff --git a/_examples/websocket/native-messages/main.go b/_examples/websocket/native-messages/main.go index 33de1d56..900b08cb 100644 --- a/_examples/websocket/native-messages/main.go +++ b/_examples/websocket/native-messages/main.go @@ -35,7 +35,7 @@ func main() { // see the inline javascript code i the websockets.html, this endpoint is used to connect to the server. app.Get("/my_endpoint", ws.Handler()) - app.StaticWeb("/js", "./static/js") // serve our custom javascript code + app.HandleDir("/js", "./static/js") // serve our custom javascript code app.Get("/", func(ctx iris.Context) { ctx.ViewData("", clientPage{"Client Page", "localhost:8080"}) diff --git a/_examples/websocket/secure/main.go b/_examples/websocket/secure/main.go index 5bedbb5e..ce41e87c 100644 --- a/_examples/websocket/secure/main.go +++ b/_examples/websocket/secure/main.go @@ -32,7 +32,7 @@ func main() { ctx.Write(websocket.ClientSource) }) - app.StaticWeb("/js", "./static/js") + app.HandleDir("/js", "./static/js") app.Get("/", func(ctx iris.Context) { // send our custom javascript source file before client really asks for that // using the go v1.8's HTTP/2 Push. diff --git a/_examples/websocket/third-party-socketio/main.go b/_examples/websocket/third-party-socketio/main.go index c2a762e7..ae20cc27 100644 --- a/_examples/websocket/third-party-socketio/main.go +++ b/_examples/websocket/third-party-socketio/main.go @@ -38,7 +38,7 @@ func main() { // serve the index.html and the javascript libraries at // http://localhost:8080 - app.StaticWeb("/", "./public") + app.HandleDir("/", "./public") app.Run(iris.Addr("localhost:8080"), iris.WithoutPathCorrection) } diff --git a/cache/browser.go b/cache/browser.go index c03f21f5..5cb1186b 100644 --- a/cache/browser.go +++ b/cache/browser.go @@ -92,7 +92,7 @@ const ifNoneMatchHeaderKey = "If-None-Match" // // Usage with combination of `StaticCache`: // assets := app.Party("/assets", cache.StaticCache(24 * time.Hour), ETag) -// assets.StaticWeb("/", "./assets") or StaticEmbedded("/", "./assets") or StaticEmbeddedGzip("/", "./assets"). +// assets.HandleDir("/", "./assets") // // Similar to `Cache304` but it doesn't depends on any "modified date", it uses just the ETag and If-None-Match headers. // @@ -124,7 +124,7 @@ var ETag = func(ctx context.Context) { // by watching system directories changes manually and use of the `ctx.WriteWithExpiration` // with a "modtime" based on the file modified date, // can be used on Party's that contains a static handler, -// i.e `StaticWeb`, `StaticEmbedded` or even `StaticEmbeddedGzip`. +// i.e `HandleDir`. var Cache304 = func(expiresEvery time.Duration) context.Handler { return func(ctx context.Context) { now := time.Now() diff --git a/cache/client/handler.go b/cache/client/handler.go index d0a7b893..0120241c 100644 --- a/cache/client/handler.go +++ b/cache/client/handler.go @@ -71,8 +71,6 @@ func parseLifeChanger(ctx context.Context) entry.LifeChanger { } } -///TODO: debug this and re-run the parallel tests on larger scale, -// because I think we have a bug here when `core/router#StaticWeb` is used after this middleware. func (h *Handler) ServeHTTP(ctx context.Context) { // check for pre-cache validators, if at least one of them return false // for this specific request, then skip the whole cache diff --git a/context/context.go b/context/context.go index 898fbf79..56f96302 100644 --- a/context/context.go +++ b/context/context.go @@ -209,6 +209,12 @@ type Context interface { Proceed(Handler) bool // HandlerName returns the current handler's name, helpful for debugging. HandlerName() string + // HandlerFileLine returns the current running handler's function source file and line information. + // Useful mostly when debugging. + HandlerFileLine() (file string, line int) + // RouteName returns the route name that this handler is running on. + // Note that it will return empty on not found handlers. + RouteName() string // Next calls all the next handler from the handlers chain, // it should be used inside a middleware. // @@ -626,7 +632,7 @@ type Context interface { // Note that it has nothing to do with server-side caching. // It does those checks by checking if the "If-Modified-Since" request header // sent by client or a previous server response header - // (e.g with WriteWithExpiration or StaticEmbedded or Favicon etc.) + // (e.g with WriteWithExpiration or HandleDir or Favicon etc.) // is a valid one and it's before the "modtime". // // A check for !modtime && err == nil is necessary to make sure that @@ -775,7 +781,7 @@ type Context interface { // You can define your own "Content-Type" with `context#ContentType`, before this function call. // // This function doesn't support resuming (by range), - // use ctx.SendFile or router's `StaticWeb` instead. + // use ctx.SendFile or router's `HandleDir` instead. ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error // ServeFile serves a file (to send a file, a zip for example to the client you should use the `SendFile` instead) // receives two parameters @@ -785,7 +791,7 @@ type Context interface { // You can define your own "Content-Type" with `context#ContentType`, before this function call. // // This function doesn't support resuming (by range), - // use ctx.SendFile or router's `StaticWeb` instead. + // use ctx.SendFile or router's `HandleDir` instead. // // Use it when you want to serve dynamic files to the client. ServeFile(filename string, gzipCompression bool) error @@ -1208,12 +1214,21 @@ func (ctx *context) Proceed(h Handler) bool { // HandlerName returns the current handler's name, helpful for debugging. func (ctx *context) HandlerName() string { - if name := ctx.currentRouteName; name != "" { - return name - } return HandlerName(ctx.handlers[ctx.currentHandlerIndex]) } +// HandlerFileLine returns the current running handler's function source file and line information. +// Useful mostly when debugging. +func (ctx *context) HandlerFileLine() (file string, line int) { + return HandlerFileLine(ctx.handlers[ctx.currentHandlerIndex]) +} + +// RouteName returns the route name that this handler is running on. +// Note that it will return empty on not found handlers. +func (ctx *context) RouteName() string { + return ctx.currentRouteName +} + // Next is the function that executed when `ctx.Next()` is called. // It can be changed to a customized one if needed (very advanced usage). // @@ -2476,7 +2491,7 @@ func (ctx *context) SetLastModified(modtime time.Time) { // Note that it has nothing to do with server-side caching. // It does those checks by checking if the "If-Modified-Since" request header // sent by client or a previous server response header -// (e.g with WriteWithExpiration or StaticEmbedded or Favicon etc.) +// (e.g with WriteWithExpiration or HandleDir or Favicon etc.) // is a valid one and it's before the "modtime". // // A check for !modtime && err == nil is necessary to make sure that @@ -3122,7 +3137,10 @@ func (ctx *context) ServeContent(content io.ReadSeeker, filename string, modtime return nil } - ctx.ContentType(filename) + if ctx.GetContentType() == "" { + ctx.ContentType(filename) + } + ctx.SetLastModified(modtime) var out io.Writer if gzipCompression && ctx.ClientSupportsGzip() { @@ -3150,7 +3168,7 @@ func (ctx *context) ServeContent(content io.ReadSeeker, filename string, modtime func (ctx *context) ServeFile(filename string, gzipCompression bool) error { f, err := os.Open(filename) if err != nil { - return fmt.Errorf("%d", 404) + return fmt.Errorf("%d", http.StatusNotFound) } defer f.Close() fi, _ := f.Stat() diff --git a/context/handler.go b/context/handler.go index a78423b9..7ae28914 100644 --- a/context/handler.go +++ b/context/handler.go @@ -3,6 +3,7 @@ package context import ( "reflect" "runtime" + "strings" ) // A Handler responds to an HTTP request. @@ -26,15 +27,35 @@ type Handler func(Context) // See `Handler` for more. type Handlers []Handler -// HandlerName returns the name, the handler function informations. -// Same as `context.HandlerName`. +// HandlerName returns the handler's function name. +// See `context.HandlerName` to get function name of the current running handler in the chain. func HandlerName(h Handler) string { pc := reflect.ValueOf(h).Pointer() - // l, n := runtime.FuncForPC(pc).FileLine(pc) - // return fmt.Sprintf("%s:%d", l, n) return runtime.FuncForPC(pc).Name() } +// HandlerFileLine returns the handler's file and line information. +// See `context.HandlerFileLine` to get the file, line of the current running handler in the chain. +func HandlerFileLine(h Handler) (file string, line int) { + pc := reflect.ValueOf(h).Pointer() + return runtime.FuncForPC(pc).FileLine(pc) +} + +// MainHandlerName tries to find the main handler than end-developer +// registered on the provided chain of handlers and returns its function name. +func MainHandlerName(handlers Handlers) (name string) { + for i := 0; i < len(handlers); i++ { + name = HandlerName(handlers[i]) + if !strings.HasPrefix(name, "github.com/kataras/iris") || + strings.HasPrefix(name, "github.com/kataras/iris/core/router.StripPrefix") || + strings.HasPrefix(name, "github.com/kataras/iris/core/router.FileServer") { + break + } + } + + return +} + // Filter is just a type of func(Handler) bool which reports whether an action must be performed // based on the incoming request. // diff --git a/context/response_writer.go b/context/response_writer.go index 419be652..9cd05f20 100644 --- a/context/response_writer.go +++ b/context/response_writer.go @@ -60,7 +60,7 @@ type ResponseWriter interface { // Written should returns the total length of bytes that were being written to the client. // In addition iris provides some variables to help low-level actions: // NoWritten, means that nothing were written yet and the response writer is still live. - // StatusCodeWritten, means that status code were written but no other bytes are written to the client, response writer may closed. + // StatusCodeWritten, means that status code was written but no other bytes are written to the client, response writer may closed. // > 0 means that the reply was written and it's the total number of bytes were written. Written() int diff --git a/context/route.go b/context/route.go index 7c680e7d..7db074a9 100644 --- a/context/route.go +++ b/context/route.go @@ -1,6 +1,13 @@ package context -import "github.com/kataras/iris/macro" +import ( + "os" + "path" + "path/filepath" + "strings" + + "github.com/kataras/iris/macro" +) // RouteReadOnly allows decoupled access to the current route // inside the context. @@ -42,4 +49,59 @@ type RouteReadOnly interface { // MainHandlerName returns the first registered handler for the route. MainHandlerName() string + + // StaticSites if not empty, refers to the system (or virtual if embedded) directory + // and sub directories that this "GET" route was registered to serve files and folders + // that contain index.html (a site). The index handler may registered by other + // route, manually or automatic by the framework, + // get the route by `Application#GetRouteByPath(staticSite.RequestPath)`. + StaticSites() []StaticSite +} + +// StaticSite is a structure which is used as field on the `Route` +// and route registration on the `APIBuilder#HandleDir`. +// See `GetStaticSites` and `APIBuilder#HandleDir`. +type StaticSite struct { + Dir string `json:"dir"` + RequestPath string `json:"requestPath"` +} + +// GetStaticSites search for a relative filename of "indexName" in "rootDir" and all its subdirectories +// and returns a list of structures which contains the directory found an "indexName" and the request path +// that a route should be registered to handle this "indexName". +// The request path is given by the directory which an index exists on. +func GetStaticSites(rootDir, rootRequestPath, indexName string) (sites []StaticSite) { + f, err := os.Open(rootDir) + if err != nil { + return nil + } + + list, err := f.Readdir(-1) + f.Close() + if err != nil { + return nil + } + + if len(list) == 0 { + return nil + } + + for _, l := range list { + dir := filepath.Join(rootDir, l.Name()) + + if l.IsDir() { + sites = append(sites, GetStaticSites(dir, path.Join(rootRequestPath, l.Name()), indexName)...) + continue + } + + if l.Name() == strings.TrimPrefix(indexName, "/") { + sites = append(sites, StaticSite{ + Dir: filepath.FromSlash(rootDir), + RequestPath: rootRequestPath, + }) + continue + } + } + + return } diff --git a/core/errors/reporter.go b/core/errors/reporter.go index 3a4ebf86..b38111f7 100644 --- a/core/errors/reporter.go +++ b/core/errors/reporter.go @@ -64,7 +64,7 @@ func (r *Reporter) AddErr(err error) bool { } if stackErr, ok := err.(StackError); ok { - r.addStack(stackErr.Stack()) + r.addStack("", stackErr.Stack()) } else { r.mu.Lock() r.wrapper = r.wrapper.AppendErr(err) @@ -108,7 +108,7 @@ func (r *Reporter) Describe(format string, err error) { return } if stackErr, ok := err.(StackError); ok { - r.addStack(stackErr.Stack()) + r.addStack(format, stackErr.Stack()) return } @@ -126,12 +126,15 @@ func (r *Reporter) Stack() []Error { return r.wrapper.Stack } -func (r *Reporter) addStack(stack []Error) { +func (r *Reporter) addStack(format string, stack []Error) { for _, e := range stack { if e.Error() == "" { continue } r.mu.Lock() + if format != "" { + e = New(format).Format(e) + } r.wrapper = r.wrapper.AppendErr(e) r.mu.Unlock() } diff --git a/core/router/api_builder.go b/core/router/api_builder.go index 310c89d5..13b61ec6 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -21,15 +21,15 @@ var ( // "GET", "POST", "PUT", "DELETE", "CONNECT", "HEAD", // "PATCH", "OPTIONS", "TRACE". AllMethods = []string{ - "GET", - "POST", - "PUT", - "DELETE", - "CONNECT", - "HEAD", - "PATCH", - "OPTIONS", - "TRACE", + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodDelete, + http.MethodConnect, + http.MethodHead, + http.MethodPatch, + http.MethodOptions, + http.MethodTrace, } ) @@ -37,20 +37,71 @@ var ( // all the routes. type repository struct { routes []*Route + pos map[string]int } -func (r *repository) register(route *Route) { - for _, r := range r.routes { - if r.String() == route.String() { - return // do not register any duplicates, the sooner the better. +func (repo *repository) remove(route *Route) bool { + for i, r := range repo.routes { + if r == route { + return repo.removeByIndex(i) } } - r.routes = append(r.routes, route) + return false } -func (r *repository) get(routeName string) *Route { - for _, r := range r.routes { +func (repo *repository) removeByPath(tmplPath string) bool { + if repo.pos != nil { + if idx, ok := repo.pos[tmplPath]; ok { + return repo.removeByIndex(idx) + } + } + + return false +} + +func (repo *repository) removeByName(routeName string) bool { + for i, r := range repo.routes { + if r.Name == routeName { + return repo.removeByIndex(i) + } + } + + return false +} + +func (repo *repository) removeByIndex(idx int) bool { + n := len(repo.routes) + + if n == 0 { + return false + } + + if idx >= n { + return false + } + + if n == 1 && idx == 0 { + repo.routes = repo.routes[0:0] + repo.pos = nil + return true + } + + r := repo.routes[idx] + if r == nil { + return false + } + + repo.routes = append(repo.routes[:idx], repo.routes[idx+1:]...) + if repo.pos != nil { + delete(repo.pos, r.Path) + } + + return true +} + +func (repo *repository) get(routeName string) *Route { + for _, r := range repo.routes { if r.Name == routeName { return r } @@ -58,8 +109,37 @@ func (r *repository) get(routeName string) *Route { return nil } -func (r *repository) getAll() []*Route { - return r.routes +func (repo *repository) getByPath(tmplPath string) *Route { + if repo.pos != nil { + if idx, ok := repo.pos[tmplPath]; ok { + if len(repo.routes) > idx { + return repo.routes[idx] + } + } + } + + return nil +} + +func (repo *repository) getAll() []*Route { + return repo.routes +} + +func (repo *repository) register(route *Route) { + for i, r := range repo.routes { + if route.Equal(r) { + // replace existing with the latest one. + repo.routes = append(repo.routes[:i], repo.routes[i+1:]...) + continue + } + } + + repo.routes = append(repo.routes, route) + if repo.pos == nil { + repo.pos = make(map[string]int) + } + + repo.pos[route.tmpl.Src] = len(repo.routes) - 1 } // APIBuilder the visible API for constructing the router @@ -181,17 +261,13 @@ func (api *APIBuilder) SetExecutionRules(executionRules ExecutionRules) Party { return api } -// Handle registers a route to the server's api. -// if empty method is passed then handler(s) are being registered to all methods, same as .Any. -// -// Returns a *Route, app will throw any errors later on. -func (api *APIBuilder) Handle(method string, relativePath string, handlers ...context.Handler) *Route { +func (api *APIBuilder) createRoutes(methods []string, relativePath string, handlers ...context.Handler) []*Route { // if relativePath[0] != '/' { // return nil, errors.New("path should start with slash and should not be empty") // } - if method == "" || method == "ALL" || method == "ANY" { // then use like it was .Any - return api.Any(relativePath, handlers...)[0] + if len(methods) == 0 || methods[0] == "ALL" || methods[0] == "ANY" { // then use like it was .Any + return api.Any(relativePath, handlers...) } // no clean path yet because of subdomain indicator/separator which contains a dot. @@ -206,7 +282,7 @@ func (api *APIBuilder) Handle(method string, relativePath string, handlers ...co fullpath := api.relativePath + relativePath // for now, keep the last "/" if any, "/xyz/" if len(handlers) == 0 { - api.reporter.Add("missing handlers for route %s: %s", method, fullpath) + api.reporter.Add("missing handlers for route %s: %s", strings.Join(methods, ", "), fullpath) return nil } @@ -222,7 +298,8 @@ func (api *APIBuilder) Handle(method string, relativePath string, handlers ...co mainHandlers := context.Handlers(handlers) // before join the middleware + handlers + done handlers and apply the execution rules. - possibleMainHandlerName := context.HandlerName(mainHandlers[0]) + + possibleMainHandlerName := context.MainHandlerName(mainHandlers) // TODO: for UseGlobal/DoneGlobal that doesn't work. applyExecutionRules(api.handlerExecutionRules, &beginHandlers, &doneHandlers, &mainHandlers) @@ -237,24 +314,36 @@ func (api *APIBuilder) Handle(method string, relativePath string, handlers ...co subdomain, path := splitSubdomainAndPath(fullpath) // if allowMethods are empty, then simply register with the passed, main, method. - methods := append(api.allowMethods, method) + methods = append(api.allowMethods, methods...) - var ( - route *Route // the latest one is this route registered, see methods append. - err error // not used outside of loop scope. - ) + routes := make([]*Route, len(methods), len(methods)) - for _, m := range methods { - route, err = NewRoute(m, subdomain, path, possibleMainHandlerName, routeHandlers, *api.macros) + for i, m := range methods { + route, err := NewRoute(m, subdomain, path, possibleMainHandlerName, routeHandlers, *api.macros) if err != nil { // template path parser errors: - api.reporter.Add("%v -> %s:%s:%s", err, method, subdomain, path) - return nil // fail on first error. + api.reporter.Add("%v -> %s:%s:%s", err, m, subdomain, path) + continue } // Add UseGlobal & DoneGlobal Handlers - route.use(api.beginGlobalHandlers) - route.done(api.doneGlobalHandlers) + route.Use(api.beginGlobalHandlers...) + route.Done(api.doneGlobalHandlers...) + routes[i] = route + } + + return routes +} + +// Handle registers a route to the server's api. +// if empty method is passed then handler(s) are being registered to all methods, same as .Any. +// +// Returns a *Route, app will throw any errors later on. +func (api *APIBuilder) Handle(method string, relativePath string, handlers ...context.Handler) *Route { + routes := api.createRoutes([]string{method}, relativePath, handlers...) + + var route *Route // the last one is returned. + for _, route = range routes { // global api.routes.register(route) } @@ -301,6 +390,61 @@ func (api *APIBuilder) HandleMany(methodOrMulti string, relativePathorMulti stri return } +// HandleDir registers a handler that serves HTTP requests +// with the contents of a file system (physical or embedded). +// +// first parameter : the route path +// second parameter : the system or the embedded directory that needs to be served +// third parameter : not required, the directory options, set fields is optional. +// +// for more options look router.FileServer. +// +// api.HandleDir("/static", "./assets", DirOptions {ShowList: true, Gzip: true, IndexName: "index.html"}) +// +// Returns the GET *Route. +// +// Examples can be found at: https://github.com/kataras/iris/tree/master/_examples/file-server +func (api *APIBuilder) HandleDir(requestPath, directory string, opts ...DirOptions) (getRoute *Route) { + options := getDirOptions(opts...) + + h := FileServer(directory, options) + + // if subdomain, we get the full path of the path only, + // because a subdomain can have parties as well + // and we need that path to call the `StripPrefix`. + if _, fullpath := splitSubdomainAndPath(joinPath(api.relativePath, requestPath)); fullpath != "/" { + h = StripPrefix(fullpath, h) + + } + + requestPath = joinPath(requestPath, WildcardFileParam()) + routes := api.createRoutes([]string{http.MethodGet, http.MethodHead}, requestPath, h) + getRoute = routes[0] + // we get all index, including sub directories even if those + // are already managed by the static handler itself. + staticSites := context.GetStaticSites(directory, getRoute.StaticPath(), options.IndexName) + for _, s := range staticSites { + // if the end-dev did manage that index route manually already + // then skip the auto-registration. + // + // Also keep note that end-dev is still able to replace this route and manage by him/herself + // later on by a simple `Handle/Get/` call, refer to `repository#register`. + if api.GetRouteByPath(s.RequestPath) != nil { + continue + } + + routes = append(routes, api.createRoutes([]string{http.MethodGet}, s.RequestPath, h)...) + getRoute.StaticSites = append(getRoute.StaticSites, s) + } + + for _, route := range routes { + route.MainHandlerName = `HandleDir(directory: "` + directory + `")` + api.routes.register(route) + } + + return getRoute +} + // Party groups routes which may have the same prefix and share same handlers, // returns that new rich subrouter. // @@ -432,19 +576,9 @@ func (api *APIBuilder) GetRoute(routeName string) *Route { return api.routes.get(routeName) } -// GetRouteReadOnly returns the registered "read-only" route based on its name, otherwise nil. -// One note: "routeName" should be case-sensitive. Used by the context to get the current route. -// It returns an interface instead to reduce wrong usage and to keep the decoupled design between -// the context and the routes. -// Look `GetRoutesReadOnly` to fetch a list of all registered routes. -// -// Look `GetRoute` for more. -func (api *APIBuilder) GetRouteReadOnly(routeName string) context.RouteReadOnly { - r := api.GetRoute(routeName) - if r == nil { - return nil - } - return routeReadOnlyWrapper{r} +// GetRouteByPath returns the registered route based on the template path (`Route.Tmpl().Src`). +func (api *APIBuilder) GetRouteByPath(tmplPath string) *Route { + return api.routes.getByPath(tmplPath) } // GetRoutesReadOnly returns the registered routes with "read-only" access, @@ -465,6 +599,31 @@ func (api *APIBuilder) GetRoutesReadOnly() []context.RouteReadOnly { return readOnlyRoutes } +// GetRouteReadOnly returns the registered "read-only" route based on its name, otherwise nil. +// One note: "routeName" should be case-sensitive. Used by the context to get the current route. +// It returns an interface instead to reduce wrong usage and to keep the decoupled design between +// the context and the routes. +// Look `GetRoutesReadOnly` to fetch a list of all registered routes. +// +// Look `GetRoute` for more. +func (api *APIBuilder) GetRouteReadOnly(routeName string) context.RouteReadOnly { + r := api.GetRoute(routeName) + if r == nil { + return nil + } + return routeReadOnlyWrapper{r} +} + +// GetRouteReadOnlyByPath returns the registered read-only route based on the template path (`Route.Tmpl().Src`). +func (api *APIBuilder) GetRouteReadOnlyByPath(tmplPath string) context.RouteReadOnly { + r := api.GetRouteByPath(tmplPath) + if r == nil { + return nil + } + + return routeReadOnlyWrapper{r} +} + // Use appends Handler(s) to the current Party's routes and child routes. // If the current Party is the root, then it registers the middleware to all child Parties' routes too. // @@ -488,7 +647,7 @@ func (api *APIBuilder) Use(handlers ...context.Handler) { // It's always a good practise to call it right before the `Application#Run` function. func (api *APIBuilder) UseGlobal(handlers ...context.Handler) { for _, r := range api.routes.routes { - r.use(handlers) // prepend the handlers to the existing routes + r.Use(handlers...) // prepend the handlers to the existing routes } // set as begin handlers for the next routes as well. api.beginGlobalHandlers = append(api.beginGlobalHandlers, handlers...) @@ -514,7 +673,7 @@ func (api *APIBuilder) Done(handlers ...context.Handler) { // It's always a good practise to call it right before the `Application#Run` function. func (api *APIBuilder) DoneGlobal(handlers ...context.Handler) { for _, r := range api.routes.routes { - r.done(handlers) // append the handlers to the existing routes + r.Done(handlers...) // append the handlers to the existing routes } // set as done handlers for the next routes as well. api.doneGlobalHandlers = append(api.doneGlobalHandlers, handlers...) @@ -616,51 +775,9 @@ func (api *APIBuilder) Any(relativePath string, handlers ...context.Handler) (ro return } -func (api *APIBuilder) registerResourceRoute(target, reqPath string, h context.Handler) *Route { - head := api.Head(reqPath, h) - head.StaticTarget = target - get := api.Get(reqPath, h) - get.StaticTarget = target - return get -} - -// StaticHandler returns a new Handler which is ready -// to serve all kind of static files. -// -// Note: -// The only difference from package-level `StaticHandler` -// is that this `StaticHandler`` receives a request path which -// is appended to the party's relative path and stripped here. -// -// Usage: -// app := iris.New() -// ... -// mySubdomainFsServer := app.Party("mysubdomain.") -// h := mySubdomainFsServer.StaticHandler("./static_files", false, false) -// /* http://mysubdomain.mydomain.com/static/css/style.css */ -// mySubdomainFsServer.Get("/static", h) -// ... -// -func (api *APIBuilder) StaticHandler(systemPath string, showList bool, gzip bool) context.Handler { - // Note: this doesn't need to be here but we'll keep it for consistently - return StaticHandler(systemPath, showList, gzip) -} - -// StaticServe serves a directory as web resource. -// Same as `StaticWeb`. -// DEPRECATED; use `StaticWeb` or `StaticHandler` (for more options) instead. -func (api *APIBuilder) StaticServe(systemPath string, requestPath ...string) *Route { - var reqPath string - - if len(requestPath) == 0 { - reqPath = strings.Replace(systemPath, string(os.PathSeparator), "/", -1) // replaces any \ to / - reqPath = strings.Replace(reqPath, "//", "/", -1) // for any case, replaces // to / - reqPath = strings.Replace(reqPath, ".", "", -1) // replace any dots (./mypath -> /mypath) - } else { - reqPath = requestPath[0] - } - - return api.StaticWeb(reqPath, systemPath) +func (api *APIBuilder) registerResourceRoute(reqPath string, h context.Handler) *Route { + api.Head(reqPath, h) + return api.Get(reqPath, h) } // StaticContent registers a GET and HEAD method routes to the requestPath @@ -677,58 +794,7 @@ func (api *APIBuilder) StaticContent(reqPath string, cType string, content []byt } } - return api.registerResourceRoute(StaticContentTarget, reqPath, h) -} - -// StaticEmbedded used when files are distributed inside the app executable, using go-bindata mostly -// First parameter is the request path, the path which the files in the vdir will be served to, for example "/static" -// Second parameter is the (virtual) directory path, for example "./assets" (no trailing slash), -// Third parameter is the Asset function -// Forth parameter is the AssetNames function. -// -// Returns the GET *Route. -// -// Example: https://github.com/kataras/iris/tree/master/_examples/file-server/embedding-files-into-app -func (api *APIBuilder) StaticEmbedded(requestPath string, vdir string, assetFn func(name string) ([]byte, error), namesFn func() []string) *Route { - return api.staticEmbedded(requestPath, vdir, assetFn, namesFn, false) -} - -// StaticEmbeddedGzip registers a route which can serve embedded gziped files -// that are embedded using the https://github.com/kataras/bindata tool and only. -// It's 8 times faster than the `StaticEmbeddedHandler` with `go-bindata` but -// it sends gzip response only, so the client must be aware that is expecting a gzip body -// (browsers and most modern browsers do that, so you can use it without fair). -// -// First parameter is the request path, the path which the files in the vdir will be served to, for example "/static" -// Second parameter is the (virtual) directory path, for example "./assets" (no trailing slash), -// Third parameter is the GzipAsset function -// Forth parameter is the GzipAssetNames function. -// -// Example: https://github.com/kataras/iris/tree/master/_examples/file-server/embedding-gziped-files-into-app -func (api *APIBuilder) StaticEmbeddedGzip(requestPath string, vdir string, gzipAssetFn func(name string) ([]byte, error), gzipNamesFn func() []string) *Route { - return api.staticEmbedded(requestPath, vdir, gzipAssetFn, gzipNamesFn, true) -} - -// look fs.go#StaticEmbeddedHandler -func (api *APIBuilder) staticEmbedded(requestPath string, vdir string, assetFn func(name string) ([]byte, error), namesFn func() []string, assetsGziped bool) *Route { - fullpath := joinPath(api.relativePath, requestPath) - // if subdomain, - // here we get the full path of the path only, - // because a subdomain can have parties as well - // and we need that path to call the `StripPrefix`. - _, fullpath = splitSubdomainAndPath(fullpath) - - paramName := "file" - requestPath = joinPath(requestPath, WildcardParam(paramName)) - - h := StaticEmbeddedHandler(vdir, assetFn, namesFn, assetsGziped) - - if fullpath != "/" { - h = StripPrefix(fullpath, h) - } - - // it handles the subdomain(root Party) of this party as well, if any. - return api.registerResourceRoute(vdir, requestPath, h) + return api.registerResourceRoute(reqPath, h) } // errDirectoryFileNotFound returns an error with message: 'Directory or file %s couldn't found. Trace: +error trace' @@ -787,48 +853,7 @@ func (api *APIBuilder) Favicon(favPath string, requestPath ...string) *Route { reqPath = requestPath[0] } - return api.registerResourceRoute(favPath, reqPath, h) -} - -// StaticWeb returns a handler that serves HTTP requests -// with the contents of the file system rooted at directory. -// -// first parameter: the route path -// second parameter: the system directory -// -// for more options look router.StaticHandler. -// -// api.StaticWeb("/static", "./static") -// -// As a special case, the returned file server redirects any request -// ending in "/index.html" to the same path, without the final -// "/index.html", if `index.html` should be served then register a -// new route for it, i.e -// `app.Get("/static", func(ctx iris.Context){ ctx.ServeFile("./static/index.html", false) })`. -// -// StaticWeb calls the `StripPrefix(fullpath, NewStaticHandlerBuilder(systemPath).Listing(false).Build())`. -// -// Returns the GET *Route. -func (api *APIBuilder) StaticWeb(requestPath string, systemPath string) *Route { - fullpath := joinPath(api.relativePath, requestPath) - - // if subdomain, - // here we get the full path of the path only, - // because a subdomain can have parties as well - // and we need that path to call the `StripPrefix`. - _, fullpath = splitSubdomainAndPath(fullpath) - - paramName := "file" - requestPath = joinPath(requestPath, WildcardParam(paramName)) - - h := NewStaticHandlerBuilder(systemPath).Listing(false).Build() - - if fullpath != "/" { - h = StripPrefix(fullpath, h) - } - - // it handles the subdomain(root Party) of this party as well, if any. - return api.registerResourceRoute(systemPath, requestPath, h) + return api.registerResourceRoute(reqPath, h) } // OnErrorCode registers an error http status code diff --git a/core/router/deprecated.go b/core/router/deprecated.go new file mode 100644 index 00000000..d530f540 --- /dev/null +++ b/core/router/deprecated.go @@ -0,0 +1,89 @@ +package router + +import ( + "runtime" + "strings" + + "github.com/kataras/iris/context" +) + +/* + Relative to deprecation: + - party.go#L138-154 + - deprecated_example_test.go +*/ + +// https://golang.org/doc/go1.9#callersframes +func getCaller() (string, int) { + var pcs [32]uintptr + n := runtime.Callers(1, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + + for { + frame, more := frames.Next() + + if (!strings.Contains(frame.File, "github.com/kataras/iris") || + strings.Contains(frame.File, "github.com/kataras/iris/_examples") || + strings.Contains(frame.File, "github.com/iris-contrib/examples") || + (strings.Contains(frame.File, "github.com/kataras/iris/core/router") && !strings.Contains(frame.File, "deprecated.go"))) && + !strings.HasSuffix(frame.Func.Name(), ".getCaller") && !strings.Contains(frame.File, "/go/src/testing") { + return frame.File, frame.Line + } + + if !more { + break + } + } + + return "?", 0 +} + +// StaticWeb is DEPRECATED. Use HandleDir(requestPath, directory) instead. +func (api *APIBuilder) StaticWeb(requestPath string, directory string) *Route { + file, line := getCaller() + api.reporter.Add(`StaticWeb is DEPRECATED and it will be removed eventually. +Source: %s:%d +Use .HandleDir("%s", "%s") instead.`, file, line, requestPath, directory) + + return nil +} + +// StaticHandler is DEPRECATED. +// Use iris.FileServer(directory, iris.DirOptions{ShowList: true, Gzip: true}) instead. +// +// Example https://github.com/kataras/iris/tree/master/_examples/file-server/basic +func (api *APIBuilder) StaticHandler(directory string, showList bool, gzip bool) context.Handler { + file, line := getCaller() + api.reporter.Add(`StaticHandler is DEPRECATED and it will be removed eventually. +Source: %s:%d +Use iris.FileServer("%s", iris.DirOptions{ShowList: %v, Gzip: %v}) instead.`, file, line, directory, showList, gzip) + return FileServer(directory, DirOptions{ShowList: showList, Gzip: gzip}) +} + +// StaticEmbedded is DEPRECATED. +// Use HandleDir(requestPath, directory, iris.DirOptions{Asset: Asset, AssetInfo: AssetInfo, AssetNames: AssetNames}) instead. +// +// Example: https://github.com/kataras/iris/tree/master/_examples/file-server/embedding-files-into-app +func (api *APIBuilder) StaticEmbedded(requestPath string, directory string, assetFn func(name string) ([]byte, error), namesFn func() []string) *Route { + file, line := getCaller() + api.reporter.Add(`StaticEmbedded is DEPRECATED and it will be removed eventually. +It is also miss the AssetInfo bindata function, which is required now. +Source: %s:%d +Use .HandleDir("%s", "%s", iris.DirOptions{Asset: Asset, AssetInfo: AssetInfo, AssetNames: AssetNames}) instead.`, file, line, requestPath, directory) + + return nil +} + +// StaticEmbeddedGzip is DEPRECATED. +// Use HandleDir(requestPath, directory, iris.DirOptions{Gzip: true, Asset: Asset, AssetInfo: AssetInfo, AssetNames: AssetNames}) instead. +// +// Example: https://github.com/kataras/iris/tree/master/_examples/file-server/embedding-gziped-files-into-app +func (api *APIBuilder) StaticEmbeddedGzip(requestPath string, directory string, assetFn func(name string) ([]byte, error), namesFn func() []string) *Route { + file, line := getCaller() + api.reporter.Add(`StaticEmbeddedGzip is DEPRECATED and it will be removed eventually. +It is also miss the AssetInfo bindata function, which is required now. +Source: %s:%d +Use .HandleDir("%s", "%s", iris.DirOptions{Gzip: true, Asset: Asset, AssetInfo: AssetInfo, AssetNames: AssetNames}) instead.`, file, line, requestPath, directory) + + return nil +} diff --git a/core/router/deprecated_example_test.go b/core/router/deprecated_example_test.go new file mode 100644 index 00000000..bfab73da --- /dev/null +++ b/core/router/deprecated_example_test.go @@ -0,0 +1,67 @@ +package router + +import ( + "fmt" +) + +func ExampleParty_StaticWeb() { + api := NewAPIBuilder() + api.StaticWeb("/static", "./assets") + + err := api.GetReport() + if err == nil { + panic("expected report for deprecation") + } + + fmt.Print(err) + // Output: StaticWeb is DEPRECATED and it will be removed eventually. + // Source: C:/mygopath/src/github.com/kataras/iris/core/router/deprecated_example_test.go:9 + // Use .HandleDir("/static", "./assets") instead. +} + +func ExampleParty_StaticHandler() { + api := NewAPIBuilder() + api.StaticHandler("./assets", false, true) + + err := api.GetReport() + if err == nil { + panic("expected report for deprecation") + } + + fmt.Print(err) + // Output: StaticHandler is DEPRECATED and it will be removed eventually. + // Source: C:/mygopath/src/github.com/kataras/iris/core/router/deprecated_example_test.go:24 + // Use iris.FileServer("./assets", iris.DirOptions{ShowList: false, Gzip: true}) instead. +} + +func ExampleParty_StaticEmbedded() { + api := NewAPIBuilder() + api.StaticEmbedded("/static", "./assets", nil, nil) + + err := api.GetReport() + if err == nil { + panic("expected report for deprecation") + } + + fmt.Print(err) + // Output: StaticEmbedded is DEPRECATED and it will be removed eventually. + // It is also miss the AssetInfo bindata function, which is required now. + // Source: C:/mygopath/src/github.com/kataras/iris/core/router/deprecated_example_test.go:39 + // Use .HandleDir("/static", "./assets", iris.DirOptions{Asset: Asset, AssetInfo: AssetInfo, AssetNames: AssetNames}) instead. +} + +func ExampleParty_StaticEmbeddedGzip() { + api := NewAPIBuilder() + api.StaticEmbeddedGzip("/static", "./assets", nil, nil) + + err := api.GetReport() + if err == nil { + panic("expected report for deprecation") + } + + fmt.Print(err) + // Output: StaticEmbeddedGzip is DEPRECATED and it will be removed eventually. + // It is also miss the AssetInfo bindata function, which is required now. + // Source: C:/mygopath/src/github.com/kataras/iris/core/router/deprecated_example_test.go:55 + // Use .HandleDir("/static", "./assets", iris.DirOptions{Gzip: true, Asset: Asset, AssetInfo: AssetInfo, AssetNames: AssetNames}) instead. +} diff --git a/core/router/fs.go b/core/router/fs.go index 721e2965..dbe7815a 100644 --- a/core/router/fs.go +++ b/core/router/fs.go @@ -1,45 +1,198 @@ package router import ( - "errors" + "bytes" "fmt" "io" "io/ioutil" - "mime/multipart" "net/http" - "net/textproto" "net/url" "os" "path" "path/filepath" "sort" - "strconv" "strings" - "sync" "time" "github.com/kataras/iris/context" ) -// StaticEmbeddedHandler returns a Handler which can serve embedded files -// that are embedded using the go-bindata tool(assetsGziped = false) or the kataras/bindata tool (assetsGziped = true). -// -// Examples: https://github.com/kataras/iris/tree/master/_examples/file-server -func StaticEmbeddedHandler(vdir string, assetFn func(name string) ([]byte, error), namesFn func() []string, assetsGziped bool) context.Handler { - // Depends on the command the user gave to the go-bindata - // the assset path (names) may be or may not be prepended with a slash. - // What we do: we remove the ./ from the vdir which should be - // the same with the asset path (names). - // we don't pathclean, because that will prepend a slash - // go-bindata should give a correct path format. - // On serve time we check the "paramName" (which is the path after the "requestPath") - // so it has the first directory part missing, we use the "vdir" to complete it - // and match with the asset path (names). - if len(vdir) > 0 { +const indexName = "/index.html" + +type DirOptions struct { + // Defaults to "/index.html", if request path is ending with **/*/$IndexName + // then it redirects to **/*(/) which another handler is handling it, + // that another handler, called index handler, is auto-registered by the framework + // if end developer does not managed to handle it by hand. + IndexName string + // When files should served under compression. + Gzip bool + + // List the files inside the current requested directory if `IndexName` not found. + ShowList bool + // If `ShowList` is true then this function will be used instead of the default one to show the list of files of a current requested directory(dir). + DirList func(ctx context.Context, dirName string, dir http.File) error + + // When embedded. + Asset func(name string) ([]byte, error) // we need this to make it compatible os.File. + AssetInfo func(name string) (os.FileInfo, error) // we need this for range support on embedded files. + AssetNames func() []string // called once. + + // Optional validator that loops through each requested resource. + AssetValidator func(ctx context.Context, name string) bool +} + +func getDirOptions(opts ...DirOptions) (options DirOptions) { + if len(opts) > 0 { + options = opts[0] + } + + if options.IndexName == "" { + options.IndexName = indexName + } else { + options.IndexName = prefix(options.IndexName, "/") + } + + return +} + +type embeddedFile struct { + os.FileInfo + io.ReadSeeker +} + +var _ http.File = (*embeddedFile)(nil) + +func (f *embeddedFile) Close() error { + return nil +} + +// func (f *embeddedFile) Readdir(count int) ([]os.FileInfo, error) { +// // this should never happen, show dirs is already checked on the handler level before this call. +// if count != -1 { +// return nil, nil +// } + +// list := make([]os.FileInfo, len(f.dir.assetNames)) +// var err error +// for i, name := range f.dir.assetNames { +// list[i], err = f.dir.assetInfo(name) +// if err != nil { +// return nil, err +// } +// } +// return list, nil +// } + +func (f *embeddedFile) Readdir(count int) ([]os.FileInfo, error) { + return nil, nil // should never happen, read directories is done by `embeddedDir`. +} + +func (f *embeddedFile) Stat() (os.FileInfo, error) { + return f.FileInfo, nil +} + +// func (f *embeddedFile) Name() string { +// return strings.TrimLeft(f.vdir, f.FileInfo.Name()) +// } + +type embeddedFileSystem struct { + vdir string + dirNames map[string]*embeddedDir // embedded tools doesn't give that info, so we initialize it in order to support `ShowList` on embedded files as well. + + asset func(name string) ([]byte, error) + assetInfo func(name string) (os.FileInfo, error) + assetNames []string +} + +var _ http.FileSystem = (*embeddedFileSystem)(nil) + +func (fs *embeddedFileSystem) Open(name string) (http.File, error) { + // name = fs.vdir + name <- no need, check the TrimLeft(name, vdir) on names loop and the asset and assetInfo redefined on `HandleDir`. + + if d, ok := fs.dirNames[name]; ok { + return d, nil + } + + info, err := fs.assetInfo(name) + if err != nil { + return nil, err + } + b, err := fs.asset(name) + if err != nil { + return nil, err + } + return &embeddedFile{ + FileInfo: info, + ReadSeeker: bytes.NewReader(b), + }, nil +} + +type embeddedBaseFileInfo struct { + baseName string + os.FileInfo +} + +func (info *embeddedBaseFileInfo) Name() string { + return info.baseName +} + +type embeddedDir struct { + name string + modTimeUnix int64 + list []os.FileInfo + *bytes.Reader // never used, will always be nil. +} + +var _ http.File = (*embeddedDir)(nil) + +func (f *embeddedDir) Close() error { return nil } +func (f *embeddedDir) Name() string { return f.name } +func (f *embeddedDir) Size() int64 { return 0 } +func (f *embeddedDir) Mode() os.FileMode { return os.ModeDir } +func (f *embeddedDir) ModTime() time.Time { return time.Unix(f.modTimeUnix, 0) } +func (f *embeddedDir) IsDir() bool { return true } +func (f *embeddedDir) Sys() interface{} { return f } +func (f *embeddedDir) Stat() (os.FileInfo, error) { return f, nil } + +func (f *embeddedDir) Readdir(count int) ([]os.FileInfo, error) { + // this should never happen, show dirs is already checked on the handler level before this call. + if count != -1 { + return nil, nil + } + + return f.list, nil +} + +// Examples can be found at: https://github.com/kataras/iris/tree/master/_examples/file-server +func FileServer(directory string, opts ...DirOptions) context.Handler { + if directory == "" { + panic("FileServer: directory is empty. The directory parameter should point to a physical system directory or to an embedded one") + } + + options := getDirOptions(opts...) + + // `embeddedFileSystem` (if AssetInfo, Asset and AssetNames are defined) or `http.Dir`. + var fs http.FileSystem = http.Dir(directory) + + if options.Asset != nil && options.AssetInfo != nil && options.AssetNames != nil { + // Depends on the command the user gave to the go-bindata + // the assset path (names) may be or may not be prepended with a slash. + // What we do: we remove the ./ from the vdir which should be + // the same with the asset path (names). + // we don't pathclean, because that will prepend a slash + // go-bindata should give a correct path format. + // On serve time we check the "paramName" (which is the path after the "requestPath") + // so it has the first directory part missing, we use the "vdir" to complete it + // and match with the asset path (names). + vdir := directory + if vdir[0] == '.' { vdir = vdir[1:] } - if vdir[0] == '/' || vdir[0] == os.PathSeparator { // second check for /something, (or ./something if we had dot on 0 it will be removed + + // second check for /something, (or ./something if we had dot on 0 it will be removed) + if vdir[0] == '/' || vdir[0] == os.PathSeparator { vdir = vdir[1:] } @@ -50,117 +203,285 @@ func StaticEmbeddedHandler(vdir string, assetFn func(name string) ([]byte, error if trailingSlashIdx := len(vdir) - 1; vdir[trailingSlashIdx] == '/' { vdir = vdir[0:trailingSlashIdx] } - } - // collect the names we are care for, - // because not all Asset used here, we need the vdir's assets. - allNames := namesFn() + // select only the paths that we care; + // that have prefix of the directory and + // skip any unnecessary the end-dev or the 3rd party tool may set. + var names []string + for _, name := range options.AssetNames() { + // i.e: name = static/css/main.css (including the directory, see `embeddedFileSystem.vdir`) - var names []string - for _, path := range allNames { - // i.e: path = public/css/main.css + if !strings.HasPrefix(name, vdir) { + continue + } - // check if path is the path name we care for - if !strings.HasPrefix(path, vdir) { - continue + names = append(names, strings.TrimLeft(name, vdir)) } - names = append(names, path) + if len(names) == 0 { + panic("FileServer: zero embedded files") + } + + asset := func(name string) ([]byte, error) { + return options.Asset(vdir + name) + } + + assetInfo := func(name string) (os.FileInfo, error) { + return options.AssetInfo(vdir + name) + } + + dirNames := make(map[string]*embeddedDir) + + if options.ShowList { + // sort filenames by smaller path. + sort.Slice(names, func(i, j int) bool { + return strings.Count(names[j], "/") > strings.Count(names[i], "/") + }) + + for _, name := range names { + dirName := path.Dir(name) + d, ok := dirNames[dirName] + + if !ok { + d = &embeddedDir{ + name: dirName, + modTimeUnix: time.Now().Unix(), + } + dirNames[dirName] = d + } + + info, err := assetInfo(name) + if err != nil { + panic(fmt.Sprintf("FileServer: report as bug: file info: %s not found in: %s", name, dirName)) + } + d.list = append(d.list, &embeddedBaseFileInfo{path.Base(name), info}) + } + + } + + fs = &embeddedFileSystem{ + vdir: vdir, + dirNames: dirNames, + + asset: asset, + assetInfo: assetInfo, + assetNames: names, + } + } else if !DirectoryExists(directory) { + panic("FileServer: system directory: " + directory + " does not exist") } - // modtime := time.Now() - h := func(ctx context.Context) { + plainStatusCode := func(ctx context.Context, statusCode int) { + if writer, ok := ctx.ResponseWriter().(*context.GzipResponseWriter); ok && writer != nil { + writer.ResetBody() + writer.Disable() + } + ctx.StatusCode(statusCode) + } - reqPath := strings.TrimPrefix(ctx.Request().URL.Path, "/"+vdir) - // i.e : /css/main.css - - for _, path := range names { - // in order to map "/" as "/index.html" - if path == "/index.html" && reqPath == "/" { - reqPath = "/index.html" - } - - if path != vdir+reqPath { - continue - } - - cType := TypeByFilename(path) - - buf, err := assetFn(path) // remove the first slash - - if assetsGziped { - // this will add the "Vary" : "Accept-Encoding" - // and "Content-Encoding": "gzip" - // headers. - context.AddGzipHeaders(ctx.ResponseWriter()) - } + htmlReplacer := strings.NewReplacer( + "&", "&", + "<", "<", + ">", ">", + // """ is shorter than """. + `"`, """, + // "'" is shorter than "'" and apos was not in HTML until HTML5. + "'", "'", + ) + dirList := options.DirList + if dirList == nil { + dirList = func(ctx context.Context, dirName string, dir http.File) error { + dirs, err := dir.Readdir(-1) if err != nil { - continue + return err } - ctx.ContentType(cType) - if _, err := ctx.Write(buf); err != nil { - ctx.StatusCode(http.StatusInternalServerError) - ctx.StopExecution() + // dst, _ := dir.Stat() + // dirName := dst.Name() + + sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() }) + + ctx.ContentType(context.ContentHTMLHeaderValue) + ctx.WriteString("
\n")
+			for _, d := range dirs {
+				name := d.Name()
+				if d.IsDir() {
+					name += "/"
+				}
+				// name may contain '?' or '#', which must be escaped to remain
+				// part of the URL path, and not indicate the start of a query
+				// string or fragment.
+				url := url.URL{Path: joinPath("./"+dirName, name)} // edit here to redirect correctly, standard library misses that.
+				ctx.Writef("%s\n", url.String(), htmlReplacer.Replace(name))
+			}
+			ctx.WriteString("
\n") + return nil + } + } + + h := func(ctx context.Context) { + name := prefix(ctx.Request().URL.Path, "/") + ctx.Request().URL.Path = name + + gzip := options.Gzip + if !gzip { + // if false then check if the dev did something like `ctx.Gzip(true)`. + _, gzip = ctx.ResponseWriter().(*context.GzipResponseWriter) + } + + f, err := fs.Open(name) + if err != nil { + plainStatusCode(ctx, http.StatusNotFound) + return + } + defer f.Close() + + info, err := f.Stat() + if err != nil { + plainStatusCode(ctx, http.StatusNotFound) + return + } + + // use contents of index.html for directory, if present + if info.IsDir() && options.IndexName != "" { + // Note that, in contrast of the default net/http mechanism; + // here different handlers may serve the indexes + // if manually then this will block will never fire, + // if index handler are automatically registered by the framework + // then this block will be fired on indexes because the static site routes are registered using the static route's handler. + // + // End-developers must have the chance to register different logic and middlewares + // to an index file, useful on Single Page Applications. + + index := strings.TrimSuffix(name, "/") + options.IndexName + fIndex, err := fs.Open(index) + if err == nil { + defer fIndex.Close() + infoIndex, err := fIndex.Stat() + if err == nil { + info = infoIndex + f = fIndex + } + } + } + + // Still a directory? (we didn't find an index.html file) + if info.IsDir() { + if !options.ShowList { + plainStatusCode(ctx, http.StatusNotFound) + return + } + if modified, err := ctx.CheckIfModifiedSince(info.ModTime()); !modified && err == nil { + ctx.WriteNotModified() + ctx.StatusCode(http.StatusNotModified) + ctx.Next() + return + } + ctx.SetLastModified(info.ModTime()) + err = dirList(ctx, info.Name(), f) + if err != nil { + plainStatusCode(ctx, http.StatusInternalServerError) + return + } + + ctx.Next() + return + } + + // index requested, send a moved permanently status + // and navigate back to the route without the index suffix. + if strings.HasSuffix(name, options.IndexName) { + localRedirect(ctx, "./") + return + } + + if options.AssetValidator != nil { + if !options.AssetValidator(ctx, info.Name()) { + errCode := ctx.GetStatusCode() + if ctx.ResponseWriter().Written() <= context.StatusCodeWritten { + // if nothing written as body from the AssetValidator but 200 status code(which is the default), + // then we assume that the end-developer just returned false expecting this to be not found. + if errCode == http.StatusOK { + errCode = http.StatusNotFound + } + plainStatusCode(ctx, errCode) + } + return + } + } + + // try to find and send the correct content type based on the filename + // and the binary data inside "f". + detectOrWriteContentType(ctx, info.Name(), f) + + if gzip { + // set the last modified as "serveContent" does. + ctx.SetLastModified(info.ModTime()) + + // write the file to the response writer. + contents, err := ioutil.ReadAll(f) + if err != nil { + ctx.Application().Logger().Debugf("err reading file: %v", err) + plainStatusCode(ctx, http.StatusInternalServerError) + return + } + + // Use `WriteNow` instead of `Write` + // because we need to know the compressed written size before + // the `FlushResponse`. + _, err = ctx.GzipResponseWriter().Write(contents) + if err != nil { + ctx.Application().Logger().Debugf("short write: %v", err) + plainStatusCode(ctx, http.StatusInternalServerError) + return } return } - // not found or error - ctx.NotFound() + http.ServeContent(ctx.ResponseWriter(), ctx.Request(), info.Name(), info.ModTime(), f) + if serveCode := ctx.GetStatusCode(); context.StatusCodeNotSuccessful(serveCode) { + plainStatusCode(ctx, serveCode) + return + } + + ctx.Next() // fire any middleware, if any. } return h } -// StaticHandler returns a new Handler which is ready -// to serve all kind of static files. -// -// Developers can wrap this handler using the `router.StripPrefix` -// for a fixed static path when the result handler is being, finally, registered to a route. -// +// 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 +// request for a path that doesn't begin with prefix by +// replying with an HTTP 404 not found error. // // Usage: -// app := iris.New() -// ... -// fileserver := iris.StaticHandler("./static_files", false, false) -// h := router.StripPrefix("/static", fileserver) -// /* http://mydomain.com/static/css/style.css */ -// app.Get("/static/{file:path}", h) -// ... -// -func StaticHandler(systemPath string, showList bool, gzip bool) context.Handler { - return NewStaticHandlerBuilder(systemPath). - Gzip(gzip). - Listing(showList). - Build() -} +// fileserver := FileServer("./static_files", DirOptions {...}) +// h := StripPrefix("/static", fileserver) +// app.Get("/static/{f:path}", h) +// app.Head("/static/{f:path}", h) +func StripPrefix(prefix string, h context.Handler) context.Handler { + if prefix == "" { + return h + } + // here we separate the path from the subdomain (if any), we care only for the path + // fixes a bug when serving static files via a subdomain + canonicalPrefix := prefix + if dotWSlashIdx := strings.Index(canonicalPrefix, SubdomainPrefix); dotWSlashIdx > 0 { + canonicalPrefix = canonicalPrefix[dotWSlashIdx+1:] + } + canonicalPrefix = toWebPath(canonicalPrefix) -// StaticHandlerBuilder is the web file system's Handler builder -// use that or the iris.StaticHandler/StaticWeb methods. -type StaticHandlerBuilder interface { - Gzip(enable bool) StaticHandlerBuilder - Listing(listDirectoriesOnOff bool) StaticHandlerBuilder - Build() context.Handler -} - -// +------------------------------------------------------------+ -// | | -// | Static Builder | -// | | -// +------------------------------------------------------------+ - -type fsHandler struct { - // user options, only directory is required. - directory http.Dir - listDirectories bool - gzip bool - // these are init on the Build() call - filesystem http.FileSystem - once sync.Once - handler context.Handler - begin context.Handlers + return func(ctx context.Context) { + if p := strings.TrimPrefix(ctx.Request().URL.Path, canonicalPrefix); len(p) < len(ctx.Request().URL.Path) { + ctx.Request().URL.Path = p + h(ctx) + } else { + ctx.NotFound() + } + } } func toWebPath(systemPath string) string { @@ -183,215 +504,6 @@ func Abs(path string) string { return absPath } -// NewStaticHandlerBuilder returns a new Handler which serves static files -// supports gzip, no listing and much more -// Note that, this static builder returns a Handler -// it doesn't cares about the rest of your iris configuration. -// -// Use the iris.StaticHandler/StaticWeb in order to serve static files on more automatic way -// this builder is used by people who have more complicated application -// structure and want a fluent api to work on. -func NewStaticHandlerBuilder(dir string) StaticHandlerBuilder { - return &fsHandler{ - directory: http.Dir(Abs(dir)), - // list directories disabled by default - listDirectories: false, - } -} - -// Gzip if enable is true then gzip compression is enabled for this static directory. -// -// Defaults to false. -func (w *fsHandler) Gzip(enable bool) StaticHandlerBuilder { - w.gzip = enable - return w -} - -// Listing turn on/off the 'show files and directories'. -// -// Defaults to false. -func (w *fsHandler) Listing(listDirectoriesOnOff bool) StaticHandlerBuilder { - w.listDirectories = listDirectoriesOnOff - return w -} - -type ( - noListFile struct { - http.File - } -) - -// Overrides the Readdir of the http.File in order to disable showing a list of the dirs/files -func (n noListFile) Readdir(count int) ([]os.FileInfo, error) { - return nil, nil -} - -// Implements the http.Filesystem -// Do not call it. -func (w *fsHandler) Open(name string) (http.File, error) { - info, err := w.filesystem.Open(name) - - if err != nil { - return nil, err - } - - if !w.listDirectories { - return noListFile{info}, nil - } - - return info, nil -} - -// Build the handler (once) and returns it -func (w *fsHandler) Build() context.Handler { - // we have to ensure that Build is called ONLY one time, - // one instance per one static directory. - w.once.Do(func() { - w.filesystem = w.directory - - fileserver := func(ctx context.Context) { - upath := ctx.Request().URL.Path - if !strings.HasPrefix(upath, "/") { - upath = "/" + upath - ctx.Request().URL.Path = upath - } - - // Note the request.url.path is changed but request.RequestURI is not - // so on custom errors we use the requesturi instead. - // this can be changed. - - // take the gzip setting. - gzipEnabled := w.gzip - if !gzipEnabled { - // if false then check if the dev did something like `ctx.Gzip(true)`. - _, gzipEnabled = ctx.ResponseWriter().(*context.GzipResponseWriter) - } - - _, prevStatusCode := serveFile(ctx, - w.filesystem, - path.Clean(upath), - false, - w.listDirectories, - gzipEnabled) - - // check for any http errors after the file handler executed - if context.StatusCodeNotSuccessful(prevStatusCode) { // error found (404 or 400 or 500 usually) - if writer, ok := ctx.ResponseWriter().(*context.GzipResponseWriter); ok && writer != nil { - writer.ResetBody() - writer.Disable() - // ctx.ResponseWriter.Header().Del(contentType) // application/x-gzip sometimes lawl - // remove gzip headers - // headers := ctx.ResponseWriter.Header() - // headers[contentType] = nil - // headers["X-Content-Type-Options"] = nil - // headers[varyHeader] = nil - // headers[contentEncodingHeader] = nil - // headers[contentLength] = nil - } - // ctx.Application().Logger().Infof(errMsg) - ctx.StatusCode(prevStatusCode) - return - } - - // go to the next middleware, if any. - ctx.Next() - } - - w.handler = fileserver - }) - - return w.handler -} - -// 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 -// request for a path that doesn't begin with prefix by -// replying with an HTTP 404 not found error. -// -// Usage: -// fileserver := Party#StaticHandler("./static_files", false, false) -// h := router.StripPrefix("/static", fileserver) -// app.Get("/static/{f:path}", h) -// app.Head("/static/{f:path}", h) -func StripPrefix(prefix string, h context.Handler) context.Handler { - if prefix == "" { - return h - } - // here we separate the path from the subdomain (if any), we care only for the path - // fixes a bug when serving static files via a subdomain - fixedPrefix := prefix - if dotWSlashIdx := strings.Index(fixedPrefix, SubdomainPrefix); dotWSlashIdx > 0 { - fixedPrefix = fixedPrefix[dotWSlashIdx+1:] - } - fixedPrefix = toWebPath(fixedPrefix) - - return func(ctx context.Context) { - if p := strings.TrimPrefix(ctx.Request().URL.Path, fixedPrefix); len(p) < len(ctx.Request().URL.Path) { - ctx.Request().URL.Path = p - h(ctx) - } else { - ctx.NotFound() - } - } -} - -// +------------------------------------------------------------+ -// | | -// | serve file handler | -// | edited from net/http/fs.go in order to support GZIP with | -// | custom iris http errors and fallback to non-compressed data| -// | when not supported. | -// | | -// +------------------------------------------------------------+ - -var htmlReplacer = strings.NewReplacer( - "&", "&", - "<", "<", - ">", ">", - // """ is shorter than """. - `"`, """, - // "'" is shorter than "'" and apos was not in HTML until HTML5. - "'", "'", -) - -func dirList(ctx context.Context, f http.File) (string, int) { - dirs, err := f.Readdir(-1) - if err != nil { - // TODO: log err.Error() to the Server.ErrorLog, once it's possible - // for a handler to get at its Server via the http.ResponseWriter. See - // Issue 12438. - return "Error reading directory", http.StatusInternalServerError - - } - sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() }) - ctx.ContentType("text/html") - fmt.Fprintf(ctx.ResponseWriter(), "
\n")
-	for _, d := range dirs {
-		name := d.Name()
-		if d.IsDir() {
-			name += "/"
-		}
-		// name may contain '?' or '#', which must be escaped to remain
-		// part of the URL path, and not indicate the start of a query
-		// string or fragment.
-		url := url.URL{Path: name}
-		fmt.Fprintf(ctx.ResponseWriter(), "%s\n", url.String(), htmlReplacer.Replace(name))
-	}
-	fmt.Fprintf(ctx.ResponseWriter(), "
\n") - return "", http.StatusOK -} - -// errSeeker is returned by ServeContent's sizeFunc when the content -// doesn't seek properly. The underlying Seeker's error text isn't -// included in the sizeFunc reply so it's not sent over HTTP to end -// users. -var errSeeker = errors.New("seeker can't seek") - -// errNoOverlap is returned by serveContent's parseRange if first-byte-pos of -// all of the byte-range-spec values is greater than the content size. -var errNoOverlap = errors.New("invalid range: failed to overlap") - // The algorithm uses at most sniffLen bytes to make its decision. const sniffLen = 512 @@ -422,442 +534,6 @@ func detectOrWriteContentType(ctx context.Context, name string, content io.ReadS return ctype, nil } -// if name is empty, filename is unknown. (used for mime type, before sniffing) -// if modtime.IsZero(), modtime is unknown. -// content must be seeked to the beginning of the file. -// The sizeFunc is called at most once. Its error, if any, is sent in the HTTP response. -func serveContent(ctx context.Context, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker) (string, int) /* we could use the TransactionErrResult but prefer not to create new objects for each of the errors on static file handlers*/ { - ctx.SetLastModified(modtime) - done, rangeReq := checkPreconditions(ctx, modtime) - if done { - return "", http.StatusNotModified - } - - code := http.StatusOK - - // If Content-Type isn't set, use the file's extension to find it, but - // if the Content-Type is unset explicitly, do not sniff the type. - ctype, err := detectOrWriteContentType(ctx, name, content) - if err != nil { - return "while seeking", http.StatusInternalServerError - } - - size, err := sizeFunc() - if err != nil { - return err.Error(), http.StatusInternalServerError - } - - // handle Content-Range header. - sendSize := size - var sendContent io.Reader = content - - if size >= 0 { - ranges, err := parseRange(rangeReq, size) - if err != nil { - if err == errNoOverlap { - ctx.Header("Content-Range", fmt.Sprintf("bytes */%d", size)) - } - return err.Error(), http.StatusRequestedRangeNotSatisfiable - - } - if sumRangesSize(ranges) > size { - // The total number of bytes in all the ranges - // is larger than the size of the file by - // itself, so this is probably an attack, or a - // dumb client. Ignore the range request. - ranges = nil - } - switch { - case len(ranges) == 1: - // RFC 2616, Section 14.16: - // "When an HTTP message includes the content of a single - // range (for example, a response to a request for a - // single range, or to a request for a set of ranges - // that overlap without any holes), this content is - // transmitted with a Content-Range header, and a - // Content-Length header showing the number of bytes - // actually transferred. - // ... - // A response to a request for a single range MUST NOT - // be sent using the multipart/byteranges media type." - ra := ranges[0] - if _, err := content.Seek(ra.start, io.SeekStart); err != nil { - return err.Error(), http.StatusRequestedRangeNotSatisfiable - } - sendSize = ra.length - code = http.StatusPartialContent - ctx.Header("Content-Range", ra.contentRange(size)) - case len(ranges) > 1: - sendSize = rangesMIMESize(ranges, ctype, size) - code = http.StatusPartialContent - - pr, pw := io.Pipe() - mw := multipart.NewWriter(pw) - ctx.ContentType("multipart/byteranges; boundary=" + mw.Boundary()) - sendContent = pr - defer pr.Close() // cause writing goroutine to fail and exit if CopyN doesn't finish. - go func() { - for _, ra := range ranges { - part, err := mw.CreatePart(ra.mimeHeader(ctype, size)) - if err != nil { - pw.CloseWithError(err) - return - } - if _, err := content.Seek(ra.start, io.SeekStart); err != nil { - pw.CloseWithError(err) - return - } - if _, err := io.CopyN(part, content, ra.length); err != nil { - pw.CloseWithError(err) - return - } - } - mw.Close() - pw.Close() - }() - } - ctx.Header("Accept-Ranges", "bytes") - if ctx.ResponseWriter().Header().Get(context.ContentEncodingHeaderKey) == "" { - ctx.Header(context.ContentLengthHeaderKey, strconv.FormatInt(sendSize, 10)) - } - } - - ctx.StatusCode(code) - - if ctx.Method() != http.MethodHead { - io.CopyN(ctx.ResponseWriter(), sendContent, sendSize) - } - - return "", code -} - -func etagEmptyOrStrongMatch(rangeValue string, etagValue string) bool { - etag, _ := scanETag(rangeValue) - if etag != "" { - if etagStrongMatch(etag, etagValue) { - return true - } - return false - } - return true -} - -// scanETag determines if a syntactically valid ETag is present at s. If so, -// the ETag and remaining text after consuming ETag is returned. Otherwise, -// it returns "", "". -func scanETag(s string) (etag string, remain string) { - s = textproto.TrimString(s) - start := 0 - if strings.HasPrefix(s, "W/") { - start = 2 - } - if len(s[start:]) < 2 || s[start] != '"' { - return "", "" - } - // ETag is either W/"text" or "text". - // See RFC 7232 2.3. - for i := start + 1; i < len(s); i++ { - c := s[i] - switch { - // Character values allowed in ETags. - case c == 0x21 || c >= 0x23 && c <= 0x7E || c >= 0x80: - case c == '"': - return string(s[:i+1]), s[i+1:] - default: - break - } - } - return "", "" -} - -// etagStrongMatch reports whether a and b match using strong ETag comparison. -// Assumes a and b are valid ETags. -func etagStrongMatch(a, b string) bool { - return a == b && a != "" && a[0] == '"' -} - -// etagWeakMatch reports whether a and b match using weak ETag comparison. -// Assumes a and b are valid ETags. -func etagWeakMatch(a, b string) bool { - return strings.TrimPrefix(a, "W/") == strings.TrimPrefix(b, "W/") -} - -// condResult is the result of an HTTP request precondition check. -// See https://tools.ietf.org/html/rfc7232 section 3. -type condResult int - -const ( - condNone condResult = iota - condTrue - condFalse -) - -func checkIfMatch(ctx context.Context) condResult { - im := ctx.GetHeader("If-Match") - if im == "" { - return condNone - } - for { - im = textproto.TrimString(im) - if len(im) == 0 { - break - } - if im[0] == ',' { - im = im[1:] - continue - } - if im[0] == '*' { - return condTrue - } - etag, remain := scanETag(im) - if etag == "" { - break - } - if etagStrongMatch(etag, ctx.ResponseWriter().Header().Get("Etag")) { - return condTrue - } - im = remain - } - - return condFalse -} - -func checkIfNoneMatch(ctx context.Context) condResult { - inm := ctx.GetHeader("If-None-Match") - if inm == "" { - return condNone - } - buf := inm - for { - buf = textproto.TrimString(buf) - if len(buf) == 0 { - break - } - if buf[0] == ',' { - buf = buf[1:] - } - if buf[0] == '*' { - return condFalse - } - etag, remain := scanETag(buf) - if etag == "" { - break - } - if etagWeakMatch(etag, ctx.ResponseWriter().Header().Get("Etag")) { - return condFalse - } - buf = remain - } - return condTrue -} - -// checkPreconditions evaluates request preconditions and reports whether a precondition -// resulted in sending StatusNotModified or StatusPreconditionFailed. -func checkPreconditions(ctx context.Context, modtime time.Time) (done bool, rangeHeader string) { - // This function carefully follows RFC 7232 section 6. - ch := checkIfMatch(ctx) - if ch == condNone { - ch = checkIfUnmodifiedSince(ctx, modtime) - } - if ch == condFalse { - - ctx.StatusCode(http.StatusPreconditionFailed) - return true, "" - } - switch checkIfNoneMatch(ctx) { - case condFalse: - if ctx.Method() == http.MethodGet || ctx.Method() == http.MethodHead { - ctx.WriteNotModified() - return true, "" - } - ctx.StatusCode(http.StatusPreconditionFailed) - return true, "" - - case condNone: - if modified, err := ctx.CheckIfModifiedSince(modtime); !modified && err == nil { - ctx.WriteNotModified() - return true, "" - } - } - - rangeHeader = ctx.GetHeader("Range") - if rangeHeader != "" { - if checkIfRange(ctx, etagEmptyOrStrongMatch, modtime) == condFalse { - rangeHeader = "" - } - } - return false, rangeHeader -} - -func checkIfUnmodifiedSince(ctx context.Context, modtime time.Time) condResult { - ius := ctx.GetHeader("If-Unmodified-Since") - if ius == "" || context.IsZeroTime(modtime) { - return condNone - } - if t, err := context.ParseTime(ctx, ius); err == nil { - // The Date-Modified header truncates sub-second precision, so - // use mtime < t+1s instead of mtime <= t to check for unmodified. - if modtime.Before(t.Add(1 * time.Second)) { - return condTrue - } - return condFalse - } - return condNone -} - -func checkIfRange(ctx context.Context, etagEmptyOrStrongMatch func(ifRangeValue string, etagValue string) bool, modtime time.Time) condResult { - if ctx.Method() != http.MethodGet { - return condNone - } - ir := ctx.GetHeader("If-Range") - if ir == "" { - return condNone - } - - if etagEmptyOrStrongMatch(ir, ctx.GetHeader("Etag")) { - return condTrue - } - - // The If-Range value is typically the ETag value, but it may also be - // the modtime date. See golang.org/issue/8367. - if modtime.IsZero() { - return condFalse - } - t, err := context.ParseTime(ctx, ir) - if err != nil { - return condFalse - } - if t.Unix() == modtime.Unix() { - return condTrue - } - return condFalse -} - -const indexPage = "/index.html" - -// name is '/'-separated, not filepath.Separator. -func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bool, showList bool, gzip bool) (string, int) { - // redirect .../index.html to .../ - // can't use Redirect() because that would make the path absolute, - // which would be a problem running under StripPrefix - if strings.HasSuffix(ctx.Request().URL.Path, indexPage) { - localRedirect(ctx, "./") - return "", http.StatusMovedPermanently - } - - f, err := fs.Open(name) - if err != nil { - return err.Error(), 404 - } - defer f.Close() - - d, err := f.Stat() - if err != nil { - return err.Error(), 404 - } - - if redirect { - // redirect to canonical path: / at end of directory url - // ctx.Request.URL.Path always begins with / - url := ctx.Request().URL.Path - if d.IsDir() { - if url[len(url)-1] != '/' { - localRedirect(ctx, path.Base(url)+"/") - return "", http.StatusMovedPermanently - } - } else { - if url[len(url)-1] == '/' { - localRedirect(ctx, "../"+path.Base(url)) - return "", http.StatusMovedPermanently - } - } - } - - // redirect if the directory name doesn't end in a slash - if d.IsDir() { - url := ctx.Request().URL.Path - if url[len(url)-1] != '/' { - localRedirect(ctx, path.Base(url)+"/") - return "", http.StatusMovedPermanently - } - } - - // use contents of index.html for directory, if present - if d.IsDir() { - index := strings.TrimSuffix(name, "/") + indexPage - ff, err := fs.Open(index) - if err == nil { - defer ff.Close() - dd, err := ff.Stat() - if err == nil { - d = dd - f = ff - } - } - } - - // Still a directory? (we didn't find an index.html file) - if d.IsDir() { - if !showList { - return "", http.StatusForbidden - } - if modified, err := ctx.CheckIfModifiedSince(d.ModTime()); !modified && err == nil { - ctx.WriteNotModified() - return "", http.StatusNotModified - } - ctx.SetLastModified(d.ModTime()) - return dirList(ctx, f) - } - - // if gzip disabled then continue using content byte ranges - if !gzip { - // serveContent will check modification time - sizeFunc := func() (int64, error) { return d.Size(), nil } - return serveContent(ctx, d.Name(), d.ModTime(), sizeFunc, f) - } - - // else, set the last modified as "serveContent" does. - ctx.SetLastModified(d.ModTime()) - - // write the file to the response writer. - contents, err := ioutil.ReadAll(f) - if err != nil { - ctx.Application().Logger().Debugf("err reading file: %v", err) - return "error reading the file", http.StatusInternalServerError - } - - // Use `WriteNow` instead of `Write` - // because we need to know the compressed written size before - // the `FlushResponse`. - _, err = ctx.GzipResponseWriter().Write(contents) - if err != nil { - ctx.Application().Logger().Debugf("short write: %v", err) - return "short write", http.StatusInternalServerError - } - - // try to find and send the correct content type based on the filename - // and the binary data inside "f". - detectOrWriteContentType(ctx, d.Name(), f) - - return "", http.StatusOK -} - -// toHTTPError returns a non-specific HTTP error message and status code -// for a given non-nil error value. It's important that toHTTPError does not -// actually return err.Error(), since msg and httpStatus are returned to users, -// and historically Go's ServeContent always returned just "404 Not Found" for -// all errors. We don't want to start leaking information in error messages. -func toHTTPError(err error) (msg string, httpStatus int) { - if os.IsNotExist(err) { - return "404 page not found", http.StatusNotFound - } - if os.IsPermission(err) { - return "403 Forbidden", http.StatusForbidden - } - // Default: - return "500 Internal Server Error", http.StatusInternalServerError -} - // localRedirect gives a Moved Permanently response. // It does not convert relative paths to absolute paths like Redirect does. func localRedirect(ctx context.Context, newPath string) { @@ -869,135 +545,6 @@ func localRedirect(ctx context.Context, newPath string) { ctx.StatusCode(http.StatusMovedPermanently) } -func containsDotDot(v string) bool { - if !strings.Contains(v, "..") { - return false - } - for _, ent := range strings.FieldsFunc(v, isSlashRune) { - if ent == ".." { - return true - } - } - return false -} - -func isSlashRune(r rune) bool { return r == '/' || r == '\\' } - -// httpRange specifies the byte range to be sent to the client. -type httpRange struct { - start, length int64 -} - -func (r httpRange) contentRange(size int64) string { - return fmt.Sprintf("bytes %d-%d/%d", r.start, r.start+r.length-1, size) -} - -func (r httpRange) mimeHeader(contentType string, size int64) textproto.MIMEHeader { - return textproto.MIMEHeader{ - "Content-Range": {r.contentRange(size)}, - contentType: {contentType}, - } -} - -// parseRange parses a Range header string as per RFC 2616. -// errNoOverlap is returned if none of the ranges overlap. -func parseRange(s string, size int64) ([]httpRange, error) { - if s == "" { - return nil, nil // header not present - } - const b = "bytes=" - if !strings.HasPrefix(s, b) { - return nil, errors.New("invalid range") - } - var ranges []httpRange - noOverlap := false - for _, ra := range strings.Split(s[len(b):], ",") { - ra = strings.TrimSpace(ra) - if ra == "" { - continue - } - i := strings.Index(ra, "-") - if i < 0 { - return nil, errors.New("invalid range") - } - start, end := strings.TrimSpace(ra[:i]), strings.TrimSpace(ra[i+1:]) - var r httpRange - if start == "" { - // If no start is specified, end specifies the - // range start relative to the end of the file. - i, err := strconv.ParseInt(end, 10, 64) - if err != nil { - return nil, errors.New("invalid range") - } - if i > size { - i = size - } - r.start = size - i - r.length = size - r.start - } else { - i, err := strconv.ParseInt(start, 10, 64) - if err != nil || i < 0 { - return nil, errors.New("invalid range") - } - if i >= size { - // If the range begins after the size of the content, - // then it does not overlap. - noOverlap = true - continue - } - r.start = i - if end == "" { - // If no end is specified, range extends to end of the file. - r.length = size - r.start - } else { - i, err := strconv.ParseInt(end, 10, 64) - if err != nil || r.start > i { - return nil, errors.New("invalid range") - } - if i >= size { - i = size - 1 - } - r.length = i - r.start + 1 - } - } - ranges = append(ranges, r) - } - if noOverlap && len(ranges) == 0 { - // The specified ranges did not overlap with the content. - return nil, errNoOverlap - } - return ranges, nil -} - -// countingWriter counts how many bytes have been written to it. -type countingWriter int64 - -func (w *countingWriter) Write(p []byte) (n int, err error) { - *w += countingWriter(len(p)) - return len(p), nil -} - -// rangesMIMESize returns the number of bytes it takes to encode the -// provided ranges as a multipart response. -func rangesMIMESize(ranges []httpRange, contentType string, contentSize int64) (encSize int64) { - var w countingWriter - mw := multipart.NewWriter(&w) - for _, ra := range ranges { - mw.CreatePart(ra.mimeHeader(contentType, contentSize)) - encSize += ra.length - } - mw.Close() - encSize += int64(w) - return -} - -func sumRangesSize(ranges []httpRange) (size int64) { - for _, ra := range ranges { - size += ra.length - } - return -} - // DirectoryExists returns true if a directory(or file) exists, otherwise false func DirectoryExists(dir string) bool { if _, err := os.Stat(dir); os.IsNotExist(err) { diff --git a/core/router/handler.go b/core/router/handler.go index 97aa8a4d..bcf41cea 100644 --- a/core/router/handler.go +++ b/core/router/handler.go @@ -76,11 +76,14 @@ func NewDefaultHandler() RequestHandler { type RoutesProvider interface { // api builder GetRoutes() []*Route GetRoute(routeName string) *Route + // GetStaticSites() []*StaticSite + // Macros() *macro.Macros } func (h *routerHandler) Build(provider RoutesProvider) error { - registeredRoutes := provider.GetRoutes() h.trees = h.trees[0:0] // reset, inneed when rebuilding. + rp := errors.NewReporter() + registeredRoutes := provider.GetRoutes() // sort, subdomains go first. sort.Slice(registeredRoutes, func(i, j int) bool { @@ -111,11 +114,8 @@ func (h *routerHandler) Build(provider RoutesProvider) error { // the rest are handled inside the node return lsub1 > lsub2 - }) - rp := errors.NewReporter() - for _, r := range registeredRoutes { // build the r.Handlers based on begin and done handlers, if any. r.BuildHandlers() @@ -133,6 +133,7 @@ func (h *routerHandler) Build(provider RoutesProvider) error { rp.Add("%v -> %s", err, r.String()) continue } + golog.Debugf(r.Trace()) } diff --git a/core/router/party.go b/core/router/party.go index 5d462392..340eb604 100644 --- a/core/router/party.go +++ b/core/router/party.go @@ -120,6 +120,39 @@ type Party interface { // in order to handle more than one paths for the same controller instance. HandleMany(method string, relativePath string, handlers ...context.Handler) []*Route + // HandleDir registers a handler that serves HTTP requests + // with the contents of a file system (physical or embedded). + // + // first parameter : the route path + // second parameter : the system or the embedded directory that needs to be served + // third parameter : not required, the directory options, set fields is optional. + // + // for more options look router.FileServer. + // + // api.HandleDir("/static", "./assets", DirOptions {ShowList: true, Gzip: true, IndexName: "index.html"}) + // + // Returns the GET *Route. + // + // Examples can be found at: https://github.com/kataras/iris/tree/master/_examples/file-server + HandleDir(requestPath, directory string, opts ...DirOptions) *Route + // StaticWeb is DEPRECATED. Use HandleDir(requestPath, directory) instead. + StaticWeb(requestPath string, directory string) *Route + // StaticHandler is DEPRECATED. + // Use iris.FileServer(directory, iris.DirOptions{ShowList: true, Gzip: true}) instead. + // + // Example https://github.com/kataras/iris/tree/master/_examples/file-server/basic + StaticHandler(directory string, showList bool, gzip bool) context.Handler + // StaticEmbedded is DEPRECATED. + // Use HandleDir(requestPath, directory, iris.DirOptions{Asset: Asset, AssetInfo: AssetInfo, AssetNames: AssetNames}) instead. + // + // Example: https://github.com/kataras/iris/tree/master/_examples/file-server/embedding-files-into-app + StaticEmbedded(requestPath string, directory string, assetFn func(name string) ([]byte, error), namesFn func() []string) *Route + // StaticEmbeddedGzip is DEPRECATED. + // Use HandleDir(requestPath, directory, iris.DirOptions{Gzip: true, Asset: Asset, AssetInfo: AssetInfo, AssetNames: AssetNames}) instead. + // + // Example: https://github.com/kataras/iris/tree/master/_examples/file-server/embedding-gziped-files-into-app + StaticEmbeddedGzip(requestPath string, directory string, assetFn func(name string) ([]byte, error), namesFn func() []string) *Route + // None registers an "offline" route // see context.ExecRoute(routeName) and // party.Routes().Online(handleResultregistry.*Route, "GET") and @@ -167,61 +200,11 @@ type Party interface { // Any registers a route for ALL of the http methods // (Get,Post,Put,Head,Patch,Options,Connect,Delete). Any(registeredPath string, handlers ...context.Handler) []*Route - - // StaticHandler returns a new Handler which is ready - // to serve all kind of static files. - // - // Note: - // The only difference from package-level `StaticHandler` - // is that this `StaticHandler` receives a request path which - // is appended to the party's relative path and stripped here. - // - // Usage: - // app := iris.New() - // ... - // mySubdomainFsServer := app.Party("mysubdomain.") - // h := mySubdomainFsServer.StaticHandler("./static_files", false, false) - // /* http://mysubdomain.mydomain.com/static/css/style.css */ - // mySubdomainFsServer.Get("/static", h) - // ... - // - StaticHandler(systemPath string, showList bool, gzip bool) context.Handler - - // StaticServe serves a directory as web resource - // it's the simpliest form of the Static* functions - // Almost same usage as StaticWeb - // accepts only one required parameter which is the systemPath, - // the same path will be used to register the GET and HEAD method routes. - // If second parameter is empty, otherwise the requestPath is the second parameter - // it uses gzip compression (compression on each request, no file cache). - // - // Returns the GET *Route. - StaticServe(systemPath string, requestPath ...string) *Route // StaticContent registers a GET and HEAD method routes to the requestPath // that are ready to serve raw static bytes, memory cached. // // Returns the GET *Route. StaticContent(requestPath string, cType string, content []byte) *Route - - // StaticEmbedded used when files are distributed inside the app executable, using go-bindata mostly - // First parameter is the request path, the path which the files in the vdir will be served to, for example "/static" - // Second parameter is the (virtual) directory path, for example "./assets" - // Third parameter is the Asset function - // Forth parameter is the AssetNames function. - // - // Returns the GET *Route. - // - // Example: https://github.com/kataras/iris/tree/master/_examples/file-server/embedding-files-into-app - StaticEmbedded(requestPath string, vdir string, assetFn func(name string) ([]byte, error), namesFn func() []string) *Route - // StaticEmbeddedGzip registers a route which can serve embedded gziped files - // that are embedded using the https://github.com/kataras/bindata tool and only. - // It's 8 times faster than the `StaticEmbeddedHandler` with `go-bindata` but - // it sends gzip response only, so the client must be aware that is expecting a gzip body - // (browsers and most modern browsers do that, so you can use it without fair). - // - // - // Example: https://github.com/kataras/iris/tree/master/_examples/file-server/embedding-gziped-files-into-app - StaticEmbeddedGzip(requestPath string, vdir string, gzipAssetFn func(name string) ([]byte, error), gzipNamesFn func() []string) *Route // Favicon serves static favicon // accepts 2 parameters, second is optional // favPath (string), declare the system directory path of the __.ico @@ -234,24 +217,6 @@ type Party interface { // // Returns the GET *Route. Favicon(favPath string, requestPath ...string) *Route - // StaticWeb returns a handler that serves HTTP requests - // with the contents of the file system rooted at directory. - // - // first parameter: the route path - // second parameter: the system directory - // - // for more options look router.StaticHandler. - // - // router.StaticWeb("/static", "./static") - // - // As a special case, the returned file server redirects any request - // ending in "/index.html" to the same path, without the final - // "index.html". - // - // StaticWeb calls the `StripPrefix(fullpath, NewStaticHandlerBuilder(systemPath).Listing(false).Build())`. - // - // Returns the GET *Route. - StaticWeb(requestPath string, systemPath string) *Route // Layout overrides the parent template layout with a more specific layout for this Party. // It returns the current Party. diff --git a/core/router/path.go b/core/router/path.go index 0a8b4014..8df69db8 100644 --- a/core/router/path.go +++ b/core/router/path.go @@ -25,6 +25,15 @@ func WildcardParam(name string) string { return prefix(name, WildcardParamStart) } +// WildcardFileParam wraps a named parameter "file" with the trailing "path" macro parameter type. +// At build state this "file" parameter is prefixed with the request handler's `WildcardParamStart`. +// Created mostly for routes that serve static files to be visibly collected by +// the `Application#GetRouteReadOnly` via the `Route.Tmpl().Src` instead of +// the underline request handler's representation (`Route.Path()`). +func WildcardFileParam() string { + return "{file:path}" +} + func convertMacroTmplToNodePath(tmpl macro.Template) string { routePath := tmpl.Src if len(routePath) > 1 && routePath[len(routePath)-1] == '/' { diff --git a/core/router/route.go b/core/router/route.go index 56fe444d..a7f53a09 100644 --- a/core/router/route.go +++ b/core/router/route.go @@ -29,25 +29,19 @@ type Route struct { // Execution happens after Begin and main Handler(s), can be empty. doneHandlers context.Handlers - Path string `json:"path"` // "/api/user/{id:uint64}" + Path string `json:"path"` // the underline router's representation, i.e "/api/user/:id" // FormattedPath all dynamic named parameters (if any) replaced with %v, // used by Application to validate param values of a Route based on its name. FormattedPath string `json:"formattedPath"` - // StaticTarget if not empty, refers to the system (or virtual if embedded) directory - // that this route is serving static files/resources from - // or to a single static filename if this route created via `APIBuilder#Favicon` - // or to a `StaticContentTarget` type if this rotue created by `APIBuilder#StaticContent`. - // - // If a route is serving static files via `APIBuilder` - // there are two routes with the same dir/filename set to this field, - // one for "HEAD" and the other for the "GET" http method. - StaticTarget string + // StaticSites if not empty, refers to the system (or virtual if embedded) directory + // and sub directories that this "GET" route was registered to serve files and folders + // that contain index.html (a site). The index handler may registered by other + // route, manually or automatic by the framework, + // get the route by `Application#GetRouteByPath(staticSite.RequestPath)`. + StaticSites []context.StaticSite `json:"staticSites"` } -// StaticContentTarget used whenever a `Route#StaticTarget` refers to a raw []byte static content instead of a directory or a file. -const StaticContentTarget = "content" - // NewRoute returns a new route based on its method, // subdomain, the path (unparsed or original), // handlers and the macro container which all routes should share. @@ -69,7 +63,7 @@ func NewRoute(method, subdomain, unparsedPath, mainHandlerName string, handlers = append(context.Handlers{macroEvaluatorHandler}, handlers...) } - path = cleanPath(path) // maybe unnecessary here but who cares in this moment + path = cleanPath(path) // maybe unnecessary here. defaultName := method + subdomain + tmpl.Src formattedPath := formatPath(path) @@ -87,26 +81,26 @@ func NewRoute(method, subdomain, unparsedPath, mainHandlerName string, return route, nil } -// use adds explicit begin handlers(middleware) to this route, -// It's being called internally, it's useless for outsiders -// because `Handlers` field is exported. -// The callers of this function are: `APIBuilder#UseGlobal` and `APIBuilder#Done`. +// Use adds explicit begin handlers to this route. +// Alternatively the end-dev can prepend to the `Handlers` field. +// Should be used before the `BuildHandlers` which is +// called by the framework itself on `Application#Run` (build state). // -// BuildHandlers should be called to build the route's `Handlers`. -func (r *Route) use(handlers context.Handlers) { +// Used internally at `APIBuilder#UseGlobal` -> `beginGlobalHandlers` -> `APIBuilder#Handle`. +func (r *Route) Use(handlers ...context.Handler) { if len(handlers) == 0 { return } r.beginHandlers = append(r.beginHandlers, handlers...) } -// use adds explicit done handlers to this route. -// It's being called internally, it's useless for outsiders -// because `Handlers` field is exported. -// The callers of this function are: `APIBuilder#UseGlobal` and `APIBuilder#Done`. +// Done adds explicit finish handlers to this route. +// Alternatively the end-dev can append to the `Handlers` field. +// Should be used before the `BuildHandlers` which is +// called by the framework itself on `Application#Run` (build state). // -// BuildHandlers should be called to build the route's `Handlers`. -func (r *Route) done(handlers context.Handlers) { +// Used internally at `APIBuilder#DoneGlobal` -> `doneGlobalHandlers` -> `APIBuilder#Handle`. +func (r *Route) Done(handlers ...context.Handler) { if len(handlers) == 0 { return } @@ -161,6 +155,13 @@ func (r Route) String() string { r.Method, r.Subdomain, r.Tmpl().Src) } +// Equal compares the method, subdomaind and the +// underline representation of the route's path, +// instead of the `String` function which returns the front representation. +func (r *Route) Equal(other *Route) bool { + return r.Method == other.Method && r.Subdomain == other.Subdomain && r.Path == other.Path +} + // Tmpl returns the path template, // it contains the parsed template // for the route's path. @@ -235,12 +236,12 @@ func (r Route) StaticPath() string { if bidx == -1 || len(src) <= bidx { return src // no dynamic part found } - if bidx == 0 { // found at first index, - // but never happens because of the prepended slash + if bidx <= 1 { // found at first{...} or second index (/{...}), + // although first index should never happen because of the prepended slash. return "/" } - return src[:bidx] + return src[:bidx-1] // (/static/{...} -> /static) } // ResolvePath returns the formatted path's %v replaced with the args. @@ -272,10 +273,15 @@ func (r Route) Trace() string { } printfmt += fmt.Sprintf(" %s ", r.Tmpl().Src) + mainHandlerName := r.MainHandlerName + if !strings.HasSuffix(mainHandlerName, ")") { + mainHandlerName += "()" + } + if l := r.RegisteredHandlersLen(); l > 1 { - printfmt += fmt.Sprintf("-> %s() and %d more", r.MainHandlerName, l-1) + printfmt += fmt.Sprintf("-> %s and %d more", mainHandlerName, l-1) } else { - printfmt += fmt.Sprintf("-> %s()", r.MainHandlerName) + printfmt += fmt.Sprintf("-> %s", mainHandlerName) } // printfmt := fmt.Sprintf("%s: %s >> %s", r.Method, r.Subdomain+r.Tmpl().Src, r.MainHandlerName) @@ -316,3 +322,7 @@ func (rd routeReadOnlyWrapper) Tmpl() macro.Template { func (rd routeReadOnlyWrapper) MainHandlerName() string { return rd.Route.MainHandlerName } + +func (rd routeReadOnlyWrapper) StaticSites() []context.StaticSite { + return rd.Route.StaticSites +} diff --git a/core/router/route_test.go b/core/router/route_test.go new file mode 100644 index 00000000..58cc25f8 --- /dev/null +++ b/core/router/route_test.go @@ -0,0 +1,56 @@ +// white-box testing + +package router + +import ( + "github.com/kataras/iris/macro" + + "testing" +) + +func TestRouteStaticPath(t *testing.T) { + var tests = []struct { + tmpl string + static string + }{ + { + tmpl: "/files/{file:path}", + static: "/files", + }, + { + tmpl: "/path", + static: "/path", + }, + { + tmpl: "/path/segment", + static: "/path/segment", + }, + { + tmpl: "/path/segment/{n:int}", + static: "/path/segment", + }, + { + tmpl: "/path/{n:uint64}/{n:int}", + static: "/path", + }, + { + tmpl: "/path/{n:uint64}/static", + static: "/path", + }, + { + tmpl: "/{name}", + static: "/", + }, + { + tmpl: "/", + static: "/", + }, + } + + for i, tt := range tests { + route := Route{tmpl: macro.Template{Src: tt.tmpl}} + if expected, got := tt.static, route.StaticPath(); expected != got { + t.Fatalf("[%d:%s] expected static path to be: '%s' but got: '%s'", i, tt.tmpl, expected, got) + } + } +} diff --git a/core/router/spa.go b/core/router/spa.go deleted file mode 100644 index 0ff8ad5c..00000000 --- a/core/router/spa.go +++ /dev/null @@ -1,167 +0,0 @@ -package router - -import ( - "strings" - - "github.com/kataras/iris/context" -) - -// AssetValidator returns true if "filename" -// is asset, i.e: strings.Contains(filename, "."). -type AssetValidator func(filename string) bool - -// SPABuilder helps building a single page application server -// which serves both routes and files from the root path. -type SPABuilder struct { - // Root defaults to "/", it's the root path that explicitly set-ed, - // this can be changed if more than SPAs are used on the same - // iris router instance. - Root string - // emptyRoot can be changed with `ChangeRoot` only, - // is, statically, true if root is empty - // and if root is empty then let 404 fire from server-side anyways if - // the passed `AssetHandler` returns 404 for a specific request path. - // Defaults to false. - emptyRoot bool - - IndexNames []string - AssetHandler context.Handler - AssetValidators []AssetValidator -} - -// AddIndexName will add an index name. -// If path == $filename then it redirects to Root, which defaults to "/". -// -// It can be called BEFORE the server start. -func (s *SPABuilder) AddIndexName(filename string) *SPABuilder { - s.IndexNames = append(s.IndexNames, filename) - return s -} - -// ChangeRoot modifies the `Root` request path that is -// explicitly set-ed if the `AssetHandler` gave a Not Found (404) -// previously, if request's path is the passed "path" -// then it explicitly sets that and it retries executing the `AssetHandler`. -// -// Empty Root means that let 404 fire from server-side anyways. -// -// Change it ONLY if you use more than one typical SPAs on the same Iris Application instance. -func (s *SPABuilder) ChangeRoot(path string) *SPABuilder { - s.Root = path - s.emptyRoot = path == "" - return s -} - -// NewSPABuilder returns a new Single Page Application builder -// It does what StaticWeb or StaticEmbedded expected to do when serving files and routes at the same time -// from the root "/" path. -// -// Accepts a static asset handler, which can be an app.StaticHandler, app.StaticEmbeddedHandler... -func NewSPABuilder(assetHandler context.Handler) *SPABuilder { - if assetHandler == nil { - assetHandler = func(ctx context.Context) { - ctx.Writef("empty asset handler") - } - } - - return &SPABuilder{ - Root: "/", - IndexNames: nil, - // "IndexNames" are empty by-default, - // if the user wants to redirect to "/" from "/index.html" she/he can chage that to []string{"index.html"} manually - // or use the `StaticHandler` as "AssetHandler" which does that already. - AssetHandler: assetHandler, - AssetValidators: []AssetValidator{ - func(path string) bool { - return true // returns true by-default, if false then it fires 404. - }, - }, - } -} - -func (s *SPABuilder) isAsset(reqPath string) bool { - for _, v := range s.AssetValidators { - if !v(reqPath) { - return false - } - } - return true -} - -// Handler serves the asset handler but in addition, it makes some checks before that, -// based on the `AssetValidators` and `IndexNames`. -func (s *SPABuilder) Handler(ctx context.Context) { - path := ctx.Path() - - // make a validator call, by-default all paths are valid and this codeblock doesn't mean anything - // but for cases that users wants to bypass an asset she/he can do that by modifiying the `APIBuilder#AssetValidators` field. - // - // It's here for backwards compatibility as well, see #803. - if !s.isAsset(path) { - // it's not asset, execute the registered route's handlers - ctx.NotFound() - return - } - - for _, index := range s.IndexNames { - if strings.HasSuffix(path, index) { - if s.emptyRoot { - ctx.NotFound() - return - } - localRedirect(ctx, "."+s.Root) - // s.Root should be manually registered to a route - // (not always, only if custom handler used). - // We don't setup an index handler here, - // let full control to the developer via "AssetHandler" - // (use of middleware, manually call of the ctx.ServeFile or ctx.View etc.) - return - } - } - - s.AssetHandler(ctx) - - if context.StatusCodeNotSuccessful(ctx.GetStatusCode()) && !s.emptyRoot && path != s.Root { - // If file was not something like a javascript file, or a css or anything that - // the passed `AssetHandler` scan-ed then re-execute the `AssetHandler` - // using the `Root` as the request path (virtually). - // - // If emptyRoot is true then - // fire the response as it's, "AssetHandler" is fully responsible for it, - // client-side's router for invalid paths will not work here else read below. - // - // Author's notes: - // the server doesn't need to know all client routes, - // client-side router is responsible for any kind of invalid paths, - // so explicit set to root path. - // - // The most simple solution was to use a - // func(ctx iris.Context) { ctx.ServeFile("$PATH/index.html") } as the "AssetHandler" - // but many developers use the `StaticHandler` (as shown in the examples) - // but it was not working as expected because it (correctly) fires - // a 404 not found if a file based on the request path didn't found. - // - // We can't just do it before the "AssetHandler"'s execution - // for two main reasons: - // 1. if it's a file serve handler, like `StaticHandler` then it will never serve - // the corresponding files! - // 2. it may manually handle those things, - // don't forget that "AssetHandler" can be - // ANY iris handler, so we can't be sure what the developer may want to do there. - // - // "AssetHandler" as the "StaticHandler" a retry doesn't hurt, - // it will give us a 404 if the file didn't found very fast WITHOUT moving to the - // rest of its validation and serving implementation. - // - // Another idea would be to modify the "AssetHandler" on every `ChangeRoot` - // call, which may give us some performance (ns) benefits - // but this could be bad if root is set-ed before the "AssetHandler", - // so keep it as it's. - rootURL, err := ctx.Request().URL.Parse(s.Root) - if err == nil { - ctx.Request().URL = rootURL - s.AssetHandler(ctx) - } - - } -} diff --git a/doc.go b/doc.go index be9ff2a6..cdc73cfe 100644 --- a/doc.go +++ b/doc.go @@ -918,66 +918,19 @@ Run Static Files - // StaticServe serves a directory as web resource - // it's the simpliest form of the Static* functions - // Almost same usage as StaticWeb - // accepts only one required parameter which is the systemPath, - // the same path will be used to register the GET and HEAD method routes. - // If second parameter is empty, otherwise the requestPath is the second parameter - // it uses gzip compression (compression on each request, no file cache). + // HandleDir registers a handler that serves HTTP requests + // with the contents of a file system (physical or embedded). + // + // first parameter : the route path + // second parameter : the system or the embedded directory that needs to be served + // third parameter : not required, the directory options, set fields is optional. + // + // for more options look router.FileServer. + // + // api.HandleDir("/static", "./assets", DirOptions {ShowList: true, Gzip: true, IndexName: "index.html"}) // // Returns the GET *Route. - StaticServe(systemPath string, requestPath ...string) (*Route, error) - - // StaticContent registers a GET and HEAD method routes to the requestPath - // that are ready to serve raw static bytes, memory cached. - // - // Returns the GET *Route. - StaticContent(reqPath string, cType string, content []byte) (*Route, error) - - // StaticEmbedded used when files are distributed inside the app executable, using go-bindata mostly - // First parameter is the request path, the path which the files in the vdir will be served to, for example "/static" - // Second parameter is the (virtual) directory path, for example "./assets" - // Third parameter is the Asset function - // Forth parameter is the AssetNames function. - // - // Returns the GET *Route. - // - // Example: https://github.com/kataras/iris/tree/master/_examples/file-server/embedding-files-into-app - StaticEmbedded(requestPath string, vdir string, assetFn func(name string) ([]byte, error), namesFn func() []string) (*Route, error) - - // Favicon serves static favicon - // accepts 2 parameters, second is optional - // favPath (string), declare the system directory path of the __.ico - // requestPath (string), it's the route's path, by default this is the "/favicon.ico" because some browsers tries to get this by default first, - // you can declare your own path if you have more than one favicon (desktop, mobile and so on) - // - // this func will add a route for you which will static serve the /yuorpath/yourfile.ico to the /yourfile.ico - // (nothing special that you can't handle by yourself). - // Note that you have to call it on every favicon you have to serve automatically (desktop, mobile and so on). - // - // Returns the GET *Route. - Favicon(favPath string, requestPath ...string) (*Route, error) - - // StaticWeb returns a handler that serves HTTP requests - // with the contents of the file system rooted at directory. - // - // first parameter: the route path - // second parameter: the system directory - // third OPTIONAL parameter: the exception routes - // (= give priority to these routes instead of the static handler) - // for more options look app.StaticHandler. - // - // app.StaticWeb("/static", "./static") - // - // As a special case, the returned file server redirects any request - // ending in "/index.html" to the same path, without the final - // "index.html". - // - // StaticWeb calls the StaticHandler(systemPath, listingDirectories: false, gzip: false ). - // - // Returns the GET *Route. - StaticWeb(requestPath string, systemPath string, exceptRoutes ...*Route) (*Route, error) + HandleDir(requestPath, directory string, opts ...DirOptions) (getRoute *Route) Example code: @@ -990,19 +943,40 @@ Example code: func main() { app := iris.New() - // This will serve the ./static/favicons/ion_32_32.ico to: localhost:8080/favicon.ico - app.Favicon("./static/favicons/ion_32_32.ico") + app.Favicon("./assets/favicon.ico") - // app.Favicon("./static/favicons/ion_32_32.ico", "/favicon_48_48.ico") - // This will serve the ./static/favicons/ion_32_32.ico to: localhost:8080/favicon_48_48.ico + // first parameter is the request path + // second is the system directory + // + // app.HandleDir("/css", "./assets/css") + // app.HandleDir("/js", "./assets/js") - app.Get("/", func(ctx iris.Context) { - ctx.HTML(` press here to see the favicon.ico. - At some browsers like chrome, it should be visible at the top-left side of the browser's window, - because some browsers make requests to the /favicon.ico automatically, - so iris serves your favicon in that path too (you can change it).`) - }) // if favicon doesn't show to you, try to clear your browser's cache. + app.HandleDir("/static", "./assets", iris.DirOptions{ + // Defaults to "/index.html", if request path is ending with IndexName + // then it redirects to ../ which another handler is handling it, + // that another handler, called index handler, is auto-registered by the framework + // if end developer does not managed to handle it by hand. + IndexName: "/index.html", + // When files should served under compression. + Gzip: false, + // List the files inside the current requested directory if `IndexName` not found. + ShowList: false, + // If `ShowList` is true then this function will be used instead of the default one to show the list of files of a current requested directory(dir). + // DirList: func(ctx context.Context, dirName string, dir http.File) error { ... } + // + // Optional validator that loops through each requested resource. + // AssetValidator: func(ctx iris.Context, name string) bool { ... } + }) + // You can also register any index handler manually, order of registration does not matter: + // app.Get("/static", [...custom middleware...], func(ctx iris.Context) { + // [...custom code...] + // ctx.ServeFile("./assets/index.html", false) + // }) + // http://localhost:8080/static + // http://localhost:8080/static/css/main.css + // http://localhost:8080/static/js/jquery-2.1.1.js + // http://localhost:8080/static/favicon.ico app.Run(iris.Addr(":8080")) } diff --git a/go19.go b/go19.go index aadcb525..5be3dafc 100644 --- a/go19.go +++ b/go19.go @@ -62,8 +62,8 @@ type ( // Look the `core/router#APIBuilder` for its implementation. // // A shortcut for the `core/router#Party`, useful when `PartyFunc` is being used. - Party = router.Party - + Party = router.Party + DirOptions = router.DirOptions // ExecutionRules gives control to the execution of the route handlers outside of the handlers themselves. // Usage: // Party#SetExecutionRules(ExecutionRules { diff --git a/httptest/httptest.go b/httptest/httptest.go index 243f765b..0994aa02 100644 --- a/httptest/httptest.go +++ b/httptest/httptest.go @@ -89,7 +89,7 @@ func New(t *testing.T, app *iris.Application, setters ...OptionSetter) *httpexpe app.Logger().SetLevel(conf.LogLevel) if err := app.Build(); err != nil { - if conf.Debug && (conf.LogLevel == "disable" || conf.LogLevel == "disabled") { + if conf.LogLevel != "disable" && conf.LogLevel != "disabled" { app.Logger().Println(err.Error()) return nil } diff --git a/iris.go b/iris.go index dd788361..1e1bfa87 100644 --- a/iris.go +++ b/iris.go @@ -359,12 +359,8 @@ var ( // // A shortcut for the `context#NewConditionalHandler`. NewConditionalHandler = context.NewConditionalHandler - // StaticEmbeddedHandler returns a Handler which can serve - // embedded into executable files. - // - // - // Examples: https://github.com/kataras/iris/tree/master/_examples/file-server - StaticEmbeddedHandler = router.StaticEmbeddedHandler + + FileServer = router.FileServer // 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 @@ -372,7 +368,7 @@ var ( // replying with an HTTP 404 not found error. // // Usage: - // fileserver := Party#StaticHandler("./static_files", false, false) + // fileserver := iris.FileServer("./static_files", DirOptions {...}) // h := iris.StripPrefix("/static", fileserver) // app.Get("/static/{f:path}", h) // app.Head("/static/{f:path}", h) @@ -437,7 +433,7 @@ var ( // Developers are free to extend this method's behavior // by watching system directories changes manually and use of the `ctx.WriteWithExpiration` // with a "modtime" based on the file modified date, - // simillary to the `StaticWeb`(which sends status OK(200) and browser disk caching instead of 304). + // simillary to the `HandleDir`(which sends status OK(200) and browser disk caching instead of 304). // // A shortcut of the `cache#Cache304`. Cache304 = cache.Cache304 @@ -489,19 +485,6 @@ var ( IsErrPath = context.IsErrPath ) -// SPA accepts an "assetHandler" which can be the result of an -// app.StaticHandler or app.StaticEmbeddedHandler. -// Use that when you want to navigate from /index.html to / automatically -// it's a helper function which just makes some checks based on the `IndexNames` and `AssetValidators` -// before the assetHandler call. -// -// Example: https://github.com/kataras/iris/tree/master/_examples/file-server/single-page-application -func (app *Application) SPA(assetHandler context.Handler) *router.SPABuilder { - s := router.NewSPABuilder(assetHandler) - app.APIBuilder.HandleMany("GET HEAD", "/{f:path}", s.Handler) - return s -} - // ConfigureHost accepts one or more `host#Configuration`, these configurators functions // can access the host created by `app.Run`, // they're being executed when application is ready to being served to the public. diff --git a/middleware/logger/config.go b/middleware/logger/config.go index 991e57b4..3bb8974f 100644 --- a/middleware/logger/config.go +++ b/middleware/logger/config.go @@ -68,7 +68,10 @@ type Config struct { // LogFunc is the writer which logs are written to, // if missing the logger middleware uses the app.Logger().Infof instead. // Note that message argument can be empty. - LogFunc func(now time.Time, latency time.Duration, status, ip, method, path string, message interface{}, headerMessage interface{}) + LogFunc func(endTime time.Time, latency time.Duration, status, ip, method, path string, message interface{}, headerMessage interface{}) + // LogFuncCtx can be used instead of `LogFunc` if handlers need to customize the output based on + // custom request-time information that the LogFunc isn't aware of. + LogFuncCtx func(ctx context.Context, latency time.Duration) // Skippers used to skip the logging i.e by `ctx.Path()` and serve // the next/main handler immediately. Skippers []SkipperFunc @@ -83,15 +86,16 @@ type Config struct { // LogFunc and Skippers to nil as well. func DefaultConfig() Config { return Config{ - Status: true, - IP: true, - Method: true, - Path: true, - Query: false, - Columns: false, - LogFunc: nil, - Skippers: nil, - skip: nil, + Status: true, + IP: true, + Method: true, + Path: true, + Query: false, + Columns: false, + LogFunc: nil, + LogFuncCtx: nil, + Skippers: nil, + skip: nil, } } diff --git a/middleware/logger/logger.go b/middleware/logger/logger.go index 98d5c8fc..324c92ad 100644 --- a/middleware/logger/logger.go +++ b/middleware/logger/logger.go @@ -100,6 +100,9 @@ func (l *requestLoggerMiddleware) ServeHTTP(ctx context.Context) { if logFunc := l.config.LogFunc; logFunc != nil { logFunc(endTime, latency, status, ip, method, path, message, headerMessage) return + } else if logFuncCtx := l.config.LogFuncCtx; logFuncCtx != nil { + logFuncCtx(ctx, latency) + return } if l.config.Columns { diff --git a/typescript/_examples/editor/main.go b/typescript/_examples/editor/main.go index 59bfb4e4..f7c8023a 100644 --- a/typescript/_examples/editor/main.go +++ b/typescript/_examples/editor/main.go @@ -8,7 +8,7 @@ import ( func main() { app := iris.New() - app.StaticWeb("/scripts", "./www/scripts") // serve the scripts + app.HandleDir("/scripts", "./www/scripts") // serve the scripts // when you edit a typescript file from the alm-tools // it compiles it to javascript, have fun! diff --git a/typescript/_examples/editor/www/scripts/app.js b/typescript/_examples/editor/www/scripts/app.js new file mode 100644 index 00000000..263cbaff --- /dev/null +++ b/typescript/_examples/editor/www/scripts/app.js @@ -0,0 +1,12 @@ +var User = (function () { + function User(fullname) { + this.name = fullname; + } + User.prototype.Hi = function (msg) { + return msg + " " + this.name; + }; + return User; +}()); +var user = new User("iris web framework!"); +var hi = user.Hi("Hello"); +window.alert(hi); diff --git a/typescript/_examples/typescript/main.go b/typescript/_examples/typescript/main.go index a42a2b92..27bf000c 100644 --- a/typescript/_examples/typescript/main.go +++ b/typescript/_examples/typescript/main.go @@ -14,7 +14,7 @@ import ( func main() { app := iris.New() - app.StaticWeb("/scripts", "./www") // serve the scripts + app.HandleDir("/scripts", "./www") // serve the scripts app.Get("/", func(ctx iris.Context) { ctx.ServeFile("./www/index.html", false) diff --git a/view/django.go b/view/django.go index d989a1a2..c5bd4295 100644 --- a/view/django.go +++ b/view/django.go @@ -11,24 +11,25 @@ import ( "strings" "sync" - "github.com/flosch/pongo2" "github.com/kataras/iris/context" + + "github.com/flosch/pongo2" ) type ( - // Value conversion for pongo2.Value - Value pongo2.Value - // Error conversion for pongo2.Error - Error pongo2.Error - // FilterFunction conversion for pongo2.FilterFunction - FilterFunction func(in *Value, param *Value) (out *Value, err *Error) + // Value type alias for pongo2.Value + Value = pongo2.Value + // Error type alias for pongo2.Error + Error = pongo2.Error + // FilterFunction type alias for pongo2.FilterFunction + FilterFunction = pongo2.FilterFunction - // Parser conversion for pongo2.Parser - Parser pongo2.Parser - // Token conversion for pongo2.Token - Token pongo2.Token - // INodeTag conversion for pongo2.InodeTag - INodeTag pongo2.INodeTag + // Parser type alias for pongo2.Parser + Parser = pongo2.Parser + // Token type alias for pongo2.Token + Token = pongo2.Token + // INodeTag type alias for pongo2.InodeTag + INodeTag = pongo2.INodeTag // TagParser the function signature of the tag's parser you will have // to implement in order to create a new tag. // @@ -41,44 +42,22 @@ type ( // // Please see the Parser documentation on how to use the parser. // See `RegisterTag` for more information about writing a tag as well. - TagParser func(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error) + TagParser = pongo2.TagParser ) -// AsValue converts any given value to a view.Value. +// AsValue converts any given value to a pongo2.Value // Usually being used within own functions passed to a template // through a Context or within filter functions. -func AsValue(i interface{}) *Value { - return (*Value)(pongo2.AsValue(i)) -} +// +// Example: +// AsValue("my string") +// +// Shortcut for `pongo2.AsValue`. +var AsValue = pongo2.AsValue // AsSafeValue works like AsValue, but does not apply the 'escape' filter. -func AsSafeValue(i interface{}) *Value { - return (*Value)(pongo2.AsSafeValue(i)) -} - -// GetValue returns the `Value` as *pongo2.Value type. -// This method was added by balthild at https://github.com/kataras/iris/pull/765 -func (value *Value) GetValue() *pongo2.Value { - return (*pongo2.Value)(value) -} - -// GetError returns the `Error` as *pongo2.Error type. -// This method was added by balthild at https://github.com/kataras/iris/pull/765 -func (error *Error) GetError() *pongo2.Error { - return (*pongo2.Error)(error) -} - -// GetParser returns the `Parser` as *pongo2.Parser type. -// This method was added by balthild at https://github.com/kataras/iris/pull/765 -func (parser *Parser) GetParser() *pongo2.Parser { - return (*pongo2.Parser)(parser) -} - -// GetToken returns the `Token` as *pongo2.Token type. -// This method was added by balthild at https://github.com/kataras/iris/pull/765 -func (token *Token) GetToken() *pongo2.Token { - return (*pongo2.Token)(token) -} +// Shortcut for `pongo2.AsSafeValue`. +var AsSafeValue = pongo2.AsSafeValue type tDjangoAssetLoader struct { baseDir string @@ -200,13 +179,8 @@ func (s *DjangoEngine) RegisterFilter(filterName string, filterBody FilterFuncti return s.registerFilter(filterName, filterBody) } -func (s *DjangoEngine) registerFilter(filterName string, filterBody FilterFunction) *DjangoEngine { - fn := pongo2.FilterFunction(func(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) { - theOut, theErr := filterBody((*Value)(in), (*Value)(param)) - return (*pongo2.Value)(theOut), (*pongo2.Error)(theErr) - }) +func (s *DjangoEngine) registerFilter(filterName string, fn FilterFunction) *DjangoEngine { pongo2.RegisterFilter(filterName, fn) - return s } @@ -216,12 +190,7 @@ func (s *DjangoEngine) registerFilter(filterName string, filterBody FilterFuncti // // See http://www.florian-schlachter.de/post/pongo2/ for more about // writing filters and tags. -func (s *DjangoEngine) RegisterTag(tagName string, parserFn TagParser) error { - fn := func(doc *pongo2.Parser, start *pongo2.Token, arguments *pongo2.Parser) (pongo2.INodeTag, *pongo2.Error) { - t, err := parserFn((*Parser)(doc), (*Token)(start), (*Parser)(arguments)) - return t, (*pongo2.Error)(err) - } - +func (s *DjangoEngine) RegisterTag(tagName string, fn TagParser) error { return pongo2.RegisterTag(tagName, fn) }