add vscode extension link and badge | Some internal improvements (not completed yet)

Former-commit-id: 9bc94e90a2780ee81f8188509d98063fb3f2924b
This commit is contained in:
Gerasimos (Makis) Maropoulos 2018-01-25 03:16:49 +02:00
parent 243439af9d
commit bf13f7648a
16 changed files with 293 additions and 219 deletions

10
FAQ.md
View File

@ -10,6 +10,14 @@ Add a `badge` to your open-source projects powered by [Iris](https://iris-go.com
> The badge is optionally, of course, it is just a simple and fast way to support Iris. The badge is work of a third-party, taken from https://github.com/blob-go/blob-go which was published by our friend @clover113 and we loved it<3 > The badge is optionally, of course, it is just a simple and fast way to support Iris. The badge is work of a third-party, taken from https://github.com/blob-go/blob-go which was published by our friend @clover113 and we loved it<3
## Editors & IDEs Extensions
### Visual Studio Code <a href="https://marketplace.visualstudio.com/items?itemName=kataras2006.iris"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/2d/Visual_Studio_Code_1.18_icon.svg/2000px-Visual_Studio_Code_1.18_icon.svg.png" height="20px" width="20px" /></a>
<https://marketplace.visualstudio.com/items?itemName=kataras2006.iris>
> Please feel free to list your own Iris extension(s) here by [PR](https://github.com/kataras/iris/pulls)
## How to upgrade ## How to upgrade
```sh ```sh
@ -18,7 +26,7 @@ go get -u github.com/kataras/iris
## Learning ## Learning
More than 50 practical examples, tutorials and articles at: More than 100 practical examples, tutorials and articles at:
- https://github.com/kataras/iris/tree/master/_examples - https://github.com/kataras/iris/tree/master/_examples
- https://github.com/iris-contrib/examples - https://github.com/iris-contrib/examples

View File

@ -2,7 +2,7 @@
<img align="right" width="169px" src="https://iris-go.com/images/icon.svg?v=a" title="logo created by @merry.dii" /> <img align="right" width="169px" src="https://iris-go.com/images/icon.svg?v=a" title="logo created by @merry.dii" />
[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris)<!-- [![release](https://img.shields.io/github/release/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/releases)--> [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris)<!--[![github closed issues](https://img.shields.io/github/issues-closed-raw/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/issues?q=is%3Aissue+is%3Aclosed)--> [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) [![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris)<!-- [![release](https://img.shields.io/github/release/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/releases)--> [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://github.com/kataras/vscode-iris)<!--[![github closed issues](https://img.shields.io/github/issues-closed-raw/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/issues?q=is%3Aissue+is%3Aclosed)--> [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases)
Iris is a fast, simple yet fully featured and very efficient web framework for Go. Iris is a fast, simple yet fully featured and very efficient web framework for Go.

View File

@ -2,7 +2,7 @@
<img align="right" width="169px" src="https://iris-go.com/images/icon.svg?v=a" title="logo created by @merry.dii" /> <img align="right" width="169px" src="https://iris-go.com/images/icon.svg?v=a" title="logo created by @merry.dii" />
[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris)<!-- [![release](https://img.shields.io/github/release/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/releases)--> [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris)<!--[![github closed issues](https://img.shields.io/github/issues-closed-raw/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/issues?q=is%3Aissue+is%3Aclosed)--> [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) [![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris)<!-- [![release](https://img.shields.io/github/release/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/releases)--> [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://github.com/kataras/vscode-iris)<!--[![github closed issues](https://img.shields.io/github/issues-closed-raw/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/issues?q=is%3Aissue+is%3Aclosed)--> [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases)
Το Iris είναι ένα γρήγορο, απλό αλλά και πλήρως λειτουργικό και πολύ αποδοτικό web framework για τη Go. Το Iris είναι ένα γρήγορο, απλό αλλά και πλήρως λειτουργικό και πολύ αποδοτικό web framework για τη Go.

View File

@ -2,7 +2,7 @@
<img align="right" width="169px" src="https://iris-go.com/images/icon.svg?v=a" title="logo created by @merry.dii" /> <img align="right" width="169px" src="https://iris-go.com/images/icon.svg?v=a" title="logo created by @merry.dii" />
[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris)<!-- [![release](https://img.shields.io/github/release/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/releases)--> [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris)<!--[![github closed issues](https://img.shields.io/github/issues-closed-raw/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/issues?q=is%3Aissue+is%3Aclosed)--> [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) [![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris)<!-- [![release](https://img.shields.io/github/release/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/releases)--> [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://github.com/kataras/vscode-iris)<!--[![github closed issues](https://img.shields.io/github/issues-closed-raw/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/issues?q=is%3Aissue+is%3Aclosed)--> [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases)
Iris - это быстрая, простая, но полнофункциональная и очень эффективная веб-платформа для Go. Iris - это быстрая, простая, но полнофункциональная и очень эффективная веб-платформа для Go.

View File

@ -2,7 +2,7 @@
<img align="right" width="169px" src="https://iris-go.com/images/icon.svg?v=a" title="logo created by @merry.dii" /> <img align="right" width="169px" src="https://iris-go.com/images/icon.svg?v=a" title="logo created by @merry.dii" />
[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris)<!-- [![release](https://img.shields.io/github/release/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/releases)--> [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris)<!--[![github closed issues](https://img.shields.io/github/issues-closed-raw/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/issues?q=is%3Aissue+is%3Aclosed)--> [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) [![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris)<!-- [![release](https://img.shields.io/github/release/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/releases)--> [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://github.com/kataras/vscode-iris)<!--[![github closed issues](https://img.shields.io/github/issues-closed-raw/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/issues?q=is%3Aissue+is%3Aclosed)--> [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases)
Iris 是一款超快、简洁高效的 Go 语言 Web开发框架。 Iris 是一款超快、简洁高效的 Go 语言 Web开发框架。

View File

@ -9,7 +9,7 @@ import (
func TestCasbinWrapper(t *testing.T) { func TestCasbinWrapper(t *testing.T) {
app := newApp() app := newApp()
e := httptest.New(t, app, httptest.Debug(true)) e := httptest.New(t, app)
type ttcasbin struct { type ttcasbin struct {
username string username string
@ -43,7 +43,6 @@ func TestCasbinWrapper(t *testing.T) {
check(e, tt.method, tt.path, tt.username, tt.status) check(e, tt.method, tt.path, tt.username, tt.status)
} }
println("ADMIN ROLES")
ttAdmin := []ttcasbin{ ttAdmin := []ttcasbin{
{"cathrin", "/dataset1/item", "GET", 200}, {"cathrin", "/dataset1/item", "GET", 200},
{"cathrin", "/dataset1/item", "POST", 200}, {"cathrin", "/dataset1/item", "POST", 200},
@ -57,7 +56,6 @@ func TestCasbinWrapper(t *testing.T) {
check(e, tt.method, tt.path, tt.username, tt.status) check(e, tt.method, tt.path, tt.username, tt.status)
} }
println("ADMIN ROLE FOR cathrin DELETED")
Enforcer.DeleteRolesForUser("cathrin") Enforcer.DeleteRolesForUser("cathrin")
ttAdminDeleted := []ttcasbin{ ttAdminDeleted := []ttcasbin{

View File

@ -42,7 +42,7 @@ func getSignupForm(ctx iris.Context) {
// views/signup.html just needs a {{ .csrfField }} template tag for // views/signup.html just needs a {{ .csrfField }} template tag for
// csrf.TemplateField to inject the CSRF token into. Easy! // csrf.TemplateField to inject the CSRF token into. Easy!
ctx.ViewData(csrf.TemplateTag, csrf.TemplateField(ctx)) ctx.ViewData(csrf.TemplateTag, csrf.TemplateField(ctx))
ctx.View("views/user/signup.html") ctx.View("user/signup.html")
// We could also retrieve the token directly from csrf.Token(r) and // We could also retrieve the token directly from csrf.Token(r) and
// set it in the request header - ctx.GetHeader("X-CSRF-Token", token) // set it in the request header - ctx.GetHeader("X-CSRF-Token", token)

View File

@ -24,10 +24,7 @@ func TestSubdomainRedirectWWW(t *testing.T) {
} }
for _, test := range tests { for _, test := range tests {
req := e.GET(test.path) e.GET(test.path).Expect().Status(httptest.StatusOK).Body().Equal(test.response)
// req.WithURL("http://www." + root)
req.Expect().Status(httptest.StatusOK).Body().Equal(test.response)
} }
} }

View File

@ -30,19 +30,19 @@ func newApp() *iris.Application {
www := app.Party("www.") www := app.Party("www.")
{ {
// Just to show how you can get all routes and copy them to another
// party or subdomain:
// Get all routes that are registered so far, including all "Parties" and subdomains:
currentRoutes := app.GetRoutes()
// Register them to the www subdomain/vhost as well:
for _, r := range currentRoutes {
www.Handle(r.Method, r.Tmpl().Src, r.Handlers...)
}
// http://www.mydomain.com/hi // http://www.mydomain.com/hi
www.Get("/hi", func(ctx iris.Context) { www.Get("/hi", func(ctx iris.Context) {
ctx.Writef("hi from www.mydomain.com") ctx.Writef("hi from www.mydomain.com")
}) })
// Just to show how you can get all routes and copy them to another
// party or subdomain:
// Get all routes that are registered so far, including all "Parties" but subdomains:
currentRoutes := app.GetRoutes()
// Register them to the www subdomain/vhost as well:
for _, r := range currentRoutes {
www.Handle(r.Method, r.Path, r.Handlers...)
}
} }
// See also the "subdomains/redirect" to register redirect router wrappers between subdomains, // See also the "subdomains/redirect" to register redirect router wrappers between subdomains,
// i.e mydomain.com to www.mydomain.com (like facebook does for SEO reasons(;)). // i.e mydomain.com to www.mydomain.com (like facebook does for SEO reasons(;)).

View File

@ -41,13 +41,13 @@ func TestSubdomainWWW(t *testing.T) {
} }
host := "localhost:1111" host := "localhost:1111"
e := httptest.New(t, app, httptest.URL("http://"+host)) e := httptest.New(t, app, httptest.URL("http://"+host), httptest.Debug(false))
for _, test := range tests { for _, test := range tests {
req := e.Request(test.method, test.path) req := e.Request(test.method, test.path)
if subdomain := test.subdomain; subdomain != "" { if subdomain := test.subdomain; subdomain != "" {
req.WithURL("http://" + subdomain + "." + host) req = req.WithURL("http://" + subdomain + "." + host)
} }
req.Expect(). req.Expect().

View File

@ -891,18 +891,6 @@ type Context interface {
var _ Context = (*context)(nil) var _ Context = (*context)(nil)
// Next calls all the next handler from the handlers chain,
// it should be used inside a middleware.
func Next(ctx Context) {
if ctx.IsStopped() {
return
}
if n, handlers := ctx.HandlerIndex(-1)+1, ctx.Handlers(); n < len(handlers) {
ctx.HandlerIndex(n)
handlers[n](ctx)
}
}
// Do calls the SetHandlers(handlers) // Do calls the SetHandlers(handlers)
// and executes the first handler, // and executes the first handler,
// handlers should not be empty. // handlers should not be empty.
@ -1159,17 +1147,38 @@ func (ctx *context) HandlerName() string {
return HandlerName(ctx.handlers[ctx.currentHandlerIndex]) return HandlerName(ctx.handlers[ctx.currentHandlerIndex])
} }
// Do sets the handler index to zero, executes the first handler // Next is the function that executed when `ctx.Next()` is called.
// and the rest of the Handlers if ctx.Next() was called. // It can be changed to a customized one if needed (very advanced usage).
// func (ctx *context) Do() { //
// ctx.currentHandlerIndex = 0 // See `DefaultNext` for more information about this and why it's exported like this.
// ctx.handlers[0](ctx) // it calls this *context var Next = DefaultNext ///TODO: add an example for this usecase, i.e describe handlers and skip only file handlers.
// } // -> replaced with inline on router.go
// DefaultNext is the default function that executed on each middleware if `ctx.Next()`
// is called.
//
// DefaultNext calls the next handler from the handlers chain by registration order,
// it should be used inside a middleware.
//
// It can be changed to a customized one if needed (very advanced usage).
//
// Developers are free to customize the whole or part of the Context's implementation
// by implementing a new `context.Context` (see https://github.com/kataras/iris/tree/master/_examples/routing/custom-context)
// or by just override the `context.Next` package-level field, `context.DefaultNext` is exported
// in order to be able for developers to merge your customized version one with the default behavior as well.
func DefaultNext(ctx Context) {
if ctx.IsStopped() {
return
}
if n, handlers := ctx.HandlerIndex(-1)+1, ctx.Handlers(); n < len(handlers) {
ctx.HandlerIndex(n)
handlers[n](ctx)
}
}
// Next calls all the next handler from the handlers chain, // Next calls all the next handler from the handlers chain,
// it should be used inside a middleware. // it should be used inside a middleware.
// //
// Note: Custom context should override this method in order to be able to pass its own context.context implementation. // Note: Custom context should override this method in order to be able to pass its own context.Context implementation.
func (ctx *context) Next() { // or context.Next(ctx) func (ctx *context) Next() { // or context.Next(ctx)
Next(ctx) Next(ctx)
} }
@ -2046,29 +2055,111 @@ var (
varyHeaderKey = "Vary" varyHeaderKey = "Vary"
) )
// staticCachePassed checks the IfModifiedSince header and var unixEpochTime = time.Unix(0, 0)
// returns true if (client-side) duration has expired
func (ctx *context) staticCachePassed(modtime time.Time) bool { // IsZeroTime reports whether t is obviously unspecified (either zero or Unix()=0).
if t, err := time.Parse(ctx.Application().ConfigurationReadOnly().GetTimeFormat(), ctx.GetHeader(ifModifiedSinceHeaderKey)); err == nil && modtime.Before(t.Add(StaticCacheDuration)) { func IsZeroTime(t time.Time) bool {
ctx.writer.Header().Del(contentTypeHeaderKey) return t.IsZero() || t.Equal(unixEpochTime)
ctx.writer.Header().Del(contentLengthHeaderKey)
ctx.StatusCode(http.StatusNotModified)
return true
} }
return false
// ParseTime parses a time header (such as the Date: header),
// trying each forth formats (or three if Application's configuration's TimeFormat is defaulted)
// that are allowed by HTTP/1.1:
// Application's configuration's TimeFormat or/and http.TimeFormat,
// time.RFC850, and time.ANSIC.
//
// Look `context#FormatTime` for the opossite operation (Time to string).
var ParseTime = func(ctx Context, text string) (t time.Time, err error) {
t, err = time.Parse(ctx.Application().ConfigurationReadOnly().GetTimeFormat(), text)
if err != nil {
return http.ParseTime(text)
}
return
}
// FormatTime returns a textual representation of the time value formatted
// according to the Application's configuration's TimeFormat field
// which defines the format.
//
// Look `context#ParseTime` for the opossite operation (string to Time).
var FormatTime = func(ctx Context, t time.Time) string {
return t.Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())
}
// SetLastModified sets the "Last-Modified" based on the "modtime" input.
// If "modtime" is zero then it does nothing.
//
// It's mostly internally on core/router and context packages.
func SetLastModified(ctx Context, modtime time.Time) {
if !IsZeroTime(modtime) {
ctx.Header(lastModifiedHeaderKey, FormatTime(ctx, modtime)) // or modtime.UTC()?
}
}
// CheckIfModifiedSince checks if the response is modified since the "modtime".
// 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.)
// is a valid one and it's before the "modtime".
//
// A check for !modtime && err == nil is necessary to make sure that
// it's not modified since, because it may return false but without even
// had the chance to check the client-side (request) header due to some errors,
// like the HTTP Method is not "GET" or "HEAD" or if the "modtime" is zero
// or if parsing time from the header failed.
//
// It's mostly used internally, e.g. `context#WriteWithExpiration`.
func CheckIfModifiedSince(ctx Context, modtime time.Time) (bool, error) {
if method := ctx.Method(); method != http.MethodGet && method != http.MethodHead {
return false, errors.New("skip: method")
}
ims := ctx.GetHeader(ifModifiedSinceHeaderKey)
if ims == "" || IsZeroTime(modtime) {
return false, errors.New("skip: zero time")
}
t, err := ParseTime(ctx, ims)
if err != nil {
return false, errors.New("skip: " + err.Error())
}
// 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 false, nil
}
return true, nil
}
// WriteNotModified sends a 304 "Not Modified" status code to the client,
// it makes sure that the content type, the content length headers
// and any "ETag" are removed before the response sent.
//
// It's mostly used internally on core/router/fs.go and context methods.
func WriteNotModified(ctx Context) {
// RFC 7232 section 4.1:
// a sender SHOULD NOT generate representation metadata other than the
// above listed fields unless said metadata exists for the purpose of
// guiding cache updates (e.g.," Last-Modified" might be useful if the
// response does not have an ETag field).
h := ctx.ResponseWriter().Header()
delete(h, contentTypeHeaderKey)
delete(h, contentLengthHeaderKey)
if h.Get("Etag") != "" {
delete(h, lastModifiedHeaderKey)
}
ctx.StatusCode(http.StatusNotModified)
} }
// WriteWithExpiration like Write but it sends with an expiration datetime // WriteWithExpiration like Write but it sends with an expiration datetime
// which is refreshed every package-level `StaticCacheDuration` field. // which is refreshed every package-level `StaticCacheDuration` field.
func (ctx *context) WriteWithExpiration(body []byte, modtime time.Time) (int, error) { func (ctx *context) WriteWithExpiration(body []byte, modtime time.Time) (int, error) {
if modified, err := CheckIfModifiedSince(ctx, modtime); !modified && err == nil {
if ctx.staticCachePassed(modtime) { WriteNotModified(ctx)
return 0, nil return 0, nil
} }
modtimeFormatted := modtime.UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()) SetLastModified(ctx, modtime)
ctx.Header(lastModifiedHeaderKey, modtimeFormatted)
return ctx.writer.Write(body) return ctx.writer.Write(body)
} }
@ -2658,16 +2749,13 @@ const (
// You can define your own "Content-Type" header also, after this function call // You can define your own "Content-Type" header also, after this function call
// Doesn't implements resuming (by range), use ctx.SendFile instead // Doesn't implements resuming (by range), use ctx.SendFile instead
func (ctx *context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error { func (ctx *context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error {
if t, err := time.Parse(ctx.Application().ConfigurationReadOnly().GetTimeFormat(), ctx.GetHeader(ifModifiedSinceHeaderKey)); err == nil && modtime.Before(t.Add(1*time.Second)) { if modified, err := CheckIfModifiedSince(ctx, modtime); !modified && err == nil {
ctx.writer.Header().Del(contentTypeHeaderKey) WriteNotModified(ctx)
ctx.writer.Header().Del(contentLengthHeaderKey)
ctx.StatusCode(http.StatusNotModified)
return nil return nil
} }
ctx.ContentType(filename) ctx.ContentType(filename)
ctx.writer.Header().Set(lastModifiedHeaderKey, modtime.UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())) SetLastModified(ctx, modtime)
ctx.StatusCode(http.StatusOK)
var out io.Writer var out io.Writer
if gzipCompression && ctx.ClientSupportsGzip() { if gzipCompression && ctx.ClientSupportsGzip() {
ctx.writer.Header().Add(varyHeaderKey, acceptEncodingHeaderKey) ctx.writer.Header().Add(varyHeaderKey, acceptEncodingHeaderKey)
@ -2680,7 +2768,7 @@ func (ctx *context) ServeContent(content io.ReadSeeker, filename string, modtime
out = ctx.writer out = ctx.writer
} }
_, err := io.Copy(out, content) _, err := io.Copy(out, content)
return errServeContent.With(err) return errServeContent.With(err) ///TODO: add an int64 as return value for the content length written like other writers or let it as it's in order to keep the stable api?
} }
// ServeFile serves a view file, to send a file ( zip for example) to the client you should use the SendFile(serverfilename,clientfilename) // ServeFile serves a view file, to send a file ( zip for example) to the client you should use the SendFile(serverfilename,clientfilename)

View File

@ -629,7 +629,6 @@ func (api *APIBuilder) Favicon(favPath string, requestPath ...string) *Route {
return api.Favicon(path.Join(favPath, "favicon.ico")) return api.Favicon(path.Join(favPath, "favicon.ico"))
} }
cType := TypeByFilename(favPath)
// copy the bytes here in order to cache and not read the ico on each request. // copy the bytes here in order to cache and not read the ico on each request.
cacheFav := make([]byte, fi.Size()) cacheFav := make([]byte, fi.Size())
if _, err = f.Read(cacheFav); err != nil { if _, err = f.Read(cacheFav); err != nil {
@ -641,25 +640,14 @@ func (api *APIBuilder) Favicon(favPath string, requestPath ...string) *Route {
Format(favPath, "favicon: couldn't read the data bytes for file: "+err.Error())) Format(favPath, "favicon: couldn't read the data bytes for file: "+err.Error()))
return nil return nil
} }
modtime := ""
modtime := time.Now()
cType := TypeByFilename(favPath)
h := func(ctx context.Context) { h := func(ctx context.Context) {
if modtime == "" { ctx.ContentType(cType)
modtime = fi.ModTime().UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()) if _, err := ctx.WriteWithExpiration(cacheFav, modtime); err != nil {
}
if t, err := time.Parse(ctx.Application().ConfigurationReadOnly().GetTimeFormat(), ctx.GetHeader(ifModifiedSinceHeaderKey)); err == nil && fi.ModTime().Before(t.Add(StaticCacheDuration)) {
ctx.ResponseWriter().Header().Del(contentTypeHeaderKey)
ctx.ResponseWriter().Header().Del(contentLengthHeaderKey)
ctx.StatusCode(http.StatusNotModified)
return
}
ctx.ResponseWriter().Header().Set(contentTypeHeaderKey, cType)
ctx.ResponseWriter().Header().Set(lastModifiedHeaderKey, modtime)
ctx.StatusCode(http.StatusOK)
if _, err := ctx.Write(cacheFav); err != nil {
// ctx.Application().Logger().Infof("error while trying to serve the favicon: %s", err.Error())
ctx.StatusCode(http.StatusInternalServerError) ctx.StatusCode(http.StatusInternalServerError)
ctx.Application().Logger().Debugf("while trying to serve the favicon: %s", err.Error())
} }
} }
@ -698,16 +686,16 @@ func (api *APIBuilder) StaticWeb(requestPath string, systemPath string) *Route {
handler := func(ctx context.Context) { handler := func(ctx context.Context) {
h(ctx) h(ctx)
if ctx.GetStatusCode() >= 200 && ctx.GetStatusCode() < 400 { // if ctx.GetStatusCode() >= 200 && ctx.GetStatusCode() < 400 {
// re-check the content type here for any case, // // re-check the content type here for any case,
// although the new code does it automatically but it's good to have it here. // // although the new code does it automatically but it's good to have it here.
if _, exists := ctx.ResponseWriter().Header()["Content-Type"]; !exists { // if _, exists := ctx.ResponseWriter().Header()["Content-Type"]; !exists {
if fname := ctx.Params().Get(paramName); fname != "" { // if fname := ctx.Params().Get(paramName); fname != "" {
cType := TypeByFilename(fname) // cType := TypeByFilename(fname)
ctx.ContentType(cType) // ctx.ContentType(cType)
} // }
} // }
} // }
} }
requestPath = joinPath(requestPath, WildcardParam(paramName)) requestPath = joinPath(requestPath, WildcardParam(paramName))
@ -791,16 +779,20 @@ func (api *APIBuilder) FireErrorCode(ctx context.Context) {
api.errorCodeHandlers.Fire(ctx) api.errorCodeHandlers.Fire(ctx)
} }
// Layout oerrides the parent template layout with a more specific layout for this Party // Layout overrides the parent template layout with a more specific layout for this Party.
// returns this Party, to continue as normal // It returns the current Party.
//
// The "tmplLayoutFile" should be a relative path to the templates dir.
// Usage: // Usage:
//
// app := iris.New() // app := iris.New()
// app.RegisterView(iris.$VIEW_ENGINE("./views", ".$extension"))
// my := app.Party("/my").Layout("layouts/mylayout.html") // my := app.Party("/my").Layout("layouts/mylayout.html")
// { // my.Get("/", func(ctx iris.Context) {
// my.Get("/", func(ctx context.Context) { // ctx.View("page1.html")
// ctx.MustRender("page1.html", nil)
// }) // })
// } //
// Examples: https://github.com/kataras/iris/tree/master/_examples/view
func (api *APIBuilder) Layout(tmplLayoutFile string) Party { func (api *APIBuilder) Layout(tmplLayoutFile string) Party {
api.Use(func(ctx context.Context) { api.Use(func(ctx context.Context) {
ctx.ViewLayout(tmplLayoutFile) ctx.ViewLayout(tmplLayoutFile)
@ -811,14 +803,14 @@ func (api *APIBuilder) Layout(tmplLayoutFile string) Party {
} }
// joinHandlers uses to create a copy of all Handlers and return them in order to use inside the node // joinHandlers uses to create a copy of all Handlers and return them in order to use inside the node
func joinHandlers(Handlers1 context.Handlers, Handlers2 context.Handlers) context.Handlers { func joinHandlers(h1 context.Handlers, h2 context.Handlers) context.Handlers {
nowLen := len(Handlers1) nowLen := len(h1)
totalLen := nowLen + len(Handlers2) totalLen := nowLen + len(h2)
// create a new slice of Handlers in order to store all handlers, the already handlers(Handlers) and the new // create a new slice of Handlers in order to merge the "h1" and "h2"
newHandlers := make(context.Handlers, totalLen) newHandlers := make(context.Handlers, totalLen)
// copy the already Handlers to the just created // copy the already Handlers to the just created
copy(newHandlers, Handlers1) copy(newHandlers, h1)
// start from there we finish, and store the new Handlers too // start from there we finish, and store the new Handlers too
copy(newHandlers[nowLen:], Handlers2) copy(newHandlers[nowLen:], h2)
return newHandlers return newHandlers
} }

View File

@ -411,7 +411,7 @@ func detectOrWriteContentType(ctx context.Context, name string, content io.ReadS
// content must be seeked to the beginning of the file. // 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. // 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*/ { 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*/ {
setLastModified(ctx, modtime) context.SetLastModified(ctx, modtime)
done, rangeReq := checkPreconditions(ctx, modtime) done, rangeReq := checkPreconditions(ctx, modtime)
if done { if done {
return "", http.StatusNotModified return "", http.StatusNotModified
@ -515,6 +515,17 @@ func serveContent(ctx context.Context, name string, modtime time.Time, sizeFunc
return "", code 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, // scanETag determines if a syntactically valid ETag is present at s. If so,
// the ETag and remaining text after consuming ETag is returned. Otherwise, // the ETag and remaining text after consuming ETag is returned. Otherwise,
// it returns "", "". // it returns "", "".
@ -595,22 +606,6 @@ func checkIfMatch(ctx context.Context) condResult {
return condFalse return condFalse
} }
func checkIfUnmodifiedSince(ctx context.Context, modtime time.Time) condResult {
ius := ctx.GetHeader("If-Unmodified-Since")
if ius == "" || isZeroTime(modtime) {
return condNone
}
if t, err := http.ParseTime(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 checkIfNoneMatch(ctx context.Context) condResult { func checkIfNoneMatch(ctx context.Context) condResult {
inm := ctx.GetHeader("If-None-Match") inm := ctx.GetHeader("If-None-Match")
if inm == "" { if inm == "" {
@ -640,86 +635,6 @@ func checkIfNoneMatch(ctx context.Context) condResult {
return condTrue return condTrue
} }
func checkIfModifiedSince(ctx context.Context, modtime time.Time) condResult {
if ctx.Method() != http.MethodGet && ctx.Method() != http.MethodHead {
return condNone
}
ims := ctx.GetHeader("If-Modified-Since")
if ims == "" || isZeroTime(modtime) {
return condNone
}
t, err := http.ParseTime(ims)
if err != nil {
return condNone
}
// 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 condFalse
}
return condTrue
}
func checkIfRange(ctx context.Context, modtime time.Time) condResult {
if ctx.Method() != http.MethodGet {
return condNone
}
ir := ctx.GetHeader("If-Range")
if ir == "" {
return condNone
}
etag, _ := scanETag(ir)
if etag != "" {
if etagStrongMatch(etag, ctx.ResponseWriter().Header().Get("Etag")) {
return condTrue
}
return condFalse
}
// 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 := http.ParseTime(ir)
if err != nil {
return condFalse
}
if t.Unix() == modtime.Unix() {
return condTrue
}
return condFalse
}
var unixEpochTime = time.Unix(0, 0)
// isZeroTime reports whether t is obviously unspecified (either zero or Unix()=0).
func isZeroTime(t time.Time) bool {
return t.IsZero() || t.Equal(unixEpochTime)
}
func setLastModified(ctx context.Context, modtime time.Time) {
if !isZeroTime(modtime) {
ctx.Header(lastModifiedHeaderKey, modtime.UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()))
}
}
func writeNotModified(ctx context.Context) {
// RFC 7232 section 4.1:
// a sender SHOULD NOT generate representation metadata other than the
// above listed fields unless said metadata exists for the purpose of
// guiding cache updates (e.g., Last-Modified might be useful if the
// response does not have an ETag field).
h := ctx.ResponseWriter().Header()
delete(h, contentTypeHeaderKey)
delete(h, contentLengthHeaderKey)
if h.Get("Etag") != "" {
delete(h, "Last-Modified")
}
ctx.StatusCode(http.StatusNotModified)
}
// checkPreconditions evaluates request preconditions and reports whether a precondition // checkPreconditions evaluates request preconditions and reports whether a precondition
// resulted in sending StatusNotModified or StatusPreconditionFailed. // resulted in sending StatusNotModified or StatusPreconditionFailed.
func checkPreconditions(ctx context.Context, modtime time.Time) (done bool, rangeHeader string) { func checkPreconditions(ctx context.Context, modtime time.Time) (done bool, rangeHeader string) {
@ -736,28 +651,72 @@ func checkPreconditions(ctx context.Context, modtime time.Time) (done bool, rang
switch checkIfNoneMatch(ctx) { switch checkIfNoneMatch(ctx) {
case condFalse: case condFalse:
if ctx.Method() == http.MethodGet || ctx.Method() == http.MethodHead { if ctx.Method() == http.MethodGet || ctx.Method() == http.MethodHead {
writeNotModified(ctx) context.WriteNotModified(ctx)
return true, "" return true, ""
} }
ctx.StatusCode(http.StatusPreconditionFailed) ctx.StatusCode(http.StatusPreconditionFailed)
return true, "" return true, ""
case condNone: case condNone:
if checkIfModifiedSince(ctx, modtime) == condFalse { if modified, err := context.CheckIfModifiedSince(ctx, modtime); !modified && err == nil {
writeNotModified(ctx) context.WriteNotModified(ctx)
return true, "" return true, ""
} }
} }
rangeHeader = ctx.GetHeader("Range") rangeHeader = ctx.GetHeader("Range")
if rangeHeader != "" { if rangeHeader != "" {
if checkIfRange(ctx, modtime) == condFalse { if checkIfRange(ctx, etagEmptyOrStrongMatch, modtime) == condFalse {
rangeHeader = "" rangeHeader = ""
} }
} }
return false, 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
}
// name is '/'-separated, not filepath.Separator. // name is '/'-separated, not filepath.Separator.
func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bool, showList bool, gzip bool) (string, int) { func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bool, showList bool, gzip bool) (string, int) {
const indexPage = "/index.html" const indexPage = "/index.html"
@ -826,8 +785,8 @@ func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bo
if !showList { if !showList {
return "", http.StatusForbidden return "", http.StatusForbidden
} }
if checkIfModifiedSince(ctx, d.ModTime()) == condFalse { if modified, err := context.CheckIfModifiedSince(ctx, d.ModTime()); !modified && err == nil {
writeNotModified(ctx) context.WriteNotModified(ctx)
return "", http.StatusNotModified return "", http.StatusNotModified
} }
ctx.Header("Last-Modified", d.ModTime().UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())) ctx.Header("Last-Modified", d.ModTime().UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()))
@ -842,7 +801,7 @@ func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bo
} }
// else, set the last modified as "serveContent" does. // else, set the last modified as "serveContent" does.
setLastModified(ctx, d.ModTime()) context.SetLastModified(ctx, d.ModTime())
// write the file to the response writer. // write the file to the response writer.
contents, err := ioutil.ReadAll(f) contents, err := ioutil.ReadAll(f)

View File

@ -211,15 +211,19 @@ type Party interface {
// Returns the GET *Route. // Returns the GET *Route.
StaticWeb(requestPath string, systemPath string) *Route StaticWeb(requestPath string, systemPath string) *Route
// Layout oerrides the parent template layout with a more specific layout for this Party // Layout overrides the parent template layout with a more specific layout for this Party.
// returns this Party, to continue as normal // It returns the current Party.
//
// The "tmplLayoutFile" should be a relative path to the templates dir.
// Usage: // Usage:
//
// app := iris.New() // app := iris.New()
// app.RegisterView(iris.$VIEW_ENGINE("./views", ".$extension"))
// my := app.Party("/my").Layout("layouts/mylayout.html") // my := app.Party("/my").Layout("layouts/mylayout.html")
// { // my.Get("/", func(ctx iris.Context) {
// my.Get("/", func(ctx context.Context) { // ctx.View("page1.html")
// ctx.MustRender("page1.html", nil)
// }) // })
// } //
// Examples: https://github.com/kataras/iris/tree/master/_examples/view
Layout(tmplLayoutFile string) Party Layout(tmplLayoutFile string) Party
} }

View File

@ -81,6 +81,22 @@ func GetCustomStructWithError(ctx iris.Context) (s testCustomStruct, err error)
return return
} }
type err struct {
Status int `json:"status_code"`
Message string `json:"message"`
}
func (e err) Dispatch(ctx iris.Context) {
// write the status code based on the err's StatusCode.
ctx.StatusCode(e.Status)
// send to the client the whole object as json
ctx.JSON(e)
}
func GetCustomErrorAsDispatcher() err {
return err{iris.StatusBadRequest, "this is my error as json"}
}
func TestFuncResult(t *testing.T) { func TestFuncResult(t *testing.T) {
app := iris.New() app := iris.New()
h := New() h := New()
@ -102,6 +118,7 @@ func TestFuncResult(t *testing.T) {
app.Get("/custom/struct/with/status/not/ok", h.Handler(GetCustomStructWithStatusNotOk)) app.Get("/custom/struct/with/status/not/ok", h.Handler(GetCustomStructWithStatusNotOk))
app.Get("/custom/struct/with/content/type", h.Handler(GetCustomStructWithContentType)) app.Get("/custom/struct/with/content/type", h.Handler(GetCustomStructWithContentType))
app.Get("/custom/struct/with/error", h.Handler(GetCustomStructWithError)) app.Get("/custom/struct/with/error", h.Handler(GetCustomStructWithError))
app.Get("/custom/error/as/dispatcher", h.Handler(GetCustomErrorAsDispatcher))
e := httptest.New(t, app) e := httptest.New(t, app)
@ -149,4 +166,10 @@ func TestFuncResult(t *testing.T) {
// the content should be not JSON it should be the status code's text // the content should be not JSON it should be the status code's text
// it will fire the error's text // it will fire the error's text
Body().Equal("omit return of testCustomStruct and fire error") Body().Equal("omit return of testCustomStruct and fire error")
e.GET("/custom/error/as/dispatcher").Expect().
Status(iris.StatusBadRequest). // the default status code if error is not nil
// the content should be not JSON it should be the status code's text
// it will fire the error's text
JSON().Equal(err{iris.StatusBadRequest, "this is my error as json"})
} }

View File

@ -87,7 +87,12 @@ func New(t *testing.T, app *iris.Application, setters ...OptionSetter) *httpexpe
// set the logger or disable it (default) and disable the updater (for any case). // set the logger or disable it (default) and disable the updater (for any case).
app.Configure(iris.WithoutVersionChecker) app.Configure(iris.WithoutVersionChecker)
app.Logger().SetLevel(conf.LogLevel) app.Logger().SetLevel(conf.LogLevel)
app.Build() if err := app.Build(); err != nil {
if conf.Debug && (conf.LogLevel == "disable" || conf.LogLevel == "disabled") {
app.Logger().Println(err.Error())
return nil
}
}
testConfiguration := httpexpect.Config{ testConfiguration := httpexpect.Config{
BaseURL: conf.URL, BaseURL: conf.URL,