Skip to content

refactor context to add a BaseContext and reduce private context struct size #16455

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions modules/context/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,10 +272,9 @@ func APIContexter() func(http.Handler) http.Handler {
var locale = middleware.Locale(w, req)
var ctx = APIContext{
Context: &Context{
Resp: NewResponse(w),
Data: map[string]interface{}{},
Locale: locale,
Session: session.GetSession(req),
BaseContext: NewBaseContext(w, req, map[string]interface{}{}),
Locale: locale,
Session: session.GetSession(req),
Repo: &Repository{
PullRequest: &PullRequest{},
},
Expand Down
166 changes: 166 additions & 0 deletions modules/context/base.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package context

import (
"net/http"
"net/url"
"path"
"strconv"
"strings"
"time"

"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"

chi "github.com/go-chi/chi/v5"
)

// BaseContext represents a general context for some simple routes
type BaseContext struct {
Resp ResponseWriter
Req *http.Request
Data map[string]interface{}
}

// NewBaseContext creates a new base context
func NewBaseContext(resp http.ResponseWriter, req *http.Request, data map[string]interface{}) *BaseContext {
return &BaseContext{
Resp: NewResponse(resp),
Req: req,
Data: data,
}
}

// GetData returns the data
func (ctx *BaseContext) GetData() map[string]interface{} {
return ctx.Data
}

// HasDataKey returns true if value of given name exists.
func (ctx *BaseContext) HasDataKey(name string) bool {
_, ok := ctx.Data[name]
return ok
}

// Header returns a header
func (ctx *BaseContext) Header() http.Header {
return ctx.Resp.Header()
}

// RemoteAddr returns the client machie ip address
func (ctx *BaseContext) RemoteAddr() string {
return ctx.Req.RemoteAddr
}

// Params returns the param on route
func (ctx *BaseContext) Params(p string) string {
s, _ := url.PathUnescape(chi.URLParam(ctx.Req, strings.TrimPrefix(p, ":")))
return s
}

// ParamsInt64 returns the param on route as int64
func (ctx *BaseContext) ParamsInt64(p string) int64 {
v, _ := strconv.ParseInt(ctx.Params(p), 10, 64)
return v
}

// SetParams set params into routes
func (ctx *BaseContext) SetParams(k, v string) {
chiCtx := chi.RouteContext(ctx)
chiCtx.URLParams.Add(strings.TrimPrefix(k, ":"), url.PathEscape(v))
}

// Write writes data to webbrowser
func (ctx *BaseContext) Write(bs []byte) (int, error) {
return ctx.Resp.Write(bs)
}

// Written returns true if there are something sent to web browser
func (ctx *BaseContext) Written() bool {
return ctx.Resp.Status() > 0
}

// Status writes status code
func (ctx *BaseContext) Status(status int) {
ctx.Resp.WriteHeader(status)
}

// Deadline is part of the interface for context.Context and we pass this to the request context
func (ctx *BaseContext) Deadline() (deadline time.Time, ok bool) {
return ctx.Req.Context().Deadline()
}

// Done is part of the interface for context.Context and we pass this to the request context
func (ctx *BaseContext) Done() <-chan struct{} {
return ctx.Req.Context().Done()
}

// Err is part of the interface for context.Context and we pass this to the request context
func (ctx *BaseContext) Err() error {
return ctx.Req.Context().Err()
}

// Value is part of the interface for context.Context and we pass this to the request context
func (ctx *BaseContext) Value(key interface{}) interface{} {
return ctx.Req.Context().Value(key)
}

// Error returned an error to web browser
func (ctx *BaseContext) Error(status int, contents ...string) {
var v = http.StatusText(status)
if len(contents) > 0 {
v = contents[0]
}
http.Error(ctx.Resp, v, status)
}

// Redirect redirect the request
func (ctx *BaseContext) Redirect(location string, status ...int) {
code := http.StatusFound
if len(status) == 1 {
code = status[0]
}

http.Redirect(ctx.Resp, ctx.Req, location, code)
}

// JSON render content as JSON
func (ctx *BaseContext) JSON(status int, content interface{}) {
ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8")
ctx.Resp.WriteHeader(status)
if err := json.NewEncoder(ctx.Resp).Encode(content); err != nil {
log.Error("Render JSON failed: %v", err)
ctx.Status(http.StatusInternalServerError)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think we have a chance to call ctx.Status again here. Because there is a ctx.Resp.WriteHeader(status) above.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's really a problem except we encode the content before write header.

}
}

// PlainText render content as plain text
func (ctx *BaseContext) PlainText(status int, bs []byte) {
ctx.Resp.WriteHeader(status)
ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8")
if _, err := ctx.Resp.Write(bs); err != nil {
log.Error("Render PlainText failed: %v", err)
ctx.Status(http.StatusInternalServerError)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above

}
}

// ServeFile serves given file to response.
func (ctx *BaseContext) ServeFile(file string, names ...string) {
var name string
if len(names) > 0 {
name = names[0]
} else {
name = path.Base(file)
}
ctx.Resp.Header().Set("Content-Description", "File Transfer")
ctx.Resp.Header().Set("Content-Type", "application/octet-stream")
ctx.Resp.Header().Set("Content-Disposition", "attachment; filename="+name)
ctx.Resp.Header().Set("Content-Transfer-Encoding", "binary")
ctx.Resp.Header().Set("Expires", "0")
ctx.Resp.Header().Set("Cache-Control", "must-revalidate")
ctx.Resp.Header().Set("Pragma", "public")
http.ServeFile(ctx.Resp, ctx.Req, file)
}
138 changes: 7 additions & 131 deletions modules/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
mc "code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
Expand All @@ -34,7 +33,6 @@ import (

"gitea.com/go-chi/cache"
"gitea.com/go-chi/session"
chi "github.com/go-chi/chi/v5"
"github.com/unknwon/com"
"github.com/unknwon/i18n"
"github.com/unrolled/render"
Expand All @@ -49,9 +47,7 @@ type Render interface {

// Context represents context of a request.
type Context struct {
Resp ResponseWriter
Req *http.Request
Data map[string]interface{} // data used by MVC templates
*BaseContext
PageData map[string]interface{} // data used by JavaScript modules in one page, it's `window.config.pageData`
Render Render
translation.Locale
Expand Down Expand Up @@ -166,12 +162,6 @@ func (ctx *Context) HasError() bool {
return hasErr.(bool)
}

// HasValue returns true if value of given name exists.
func (ctx *Context) HasValue(name string) bool {
_, ok := ctx.Data[name]
return ok
}

// RedirectToFirst redirects to first not empty URL
func (ctx *Context) RedirectToFirst(location ...string) {
for _, loc := range location {
Expand Down Expand Up @@ -326,33 +316,6 @@ func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interfa
http.ServeContent(ctx.Resp, ctx.Req, name, modtime, r)
}

// PlainText render content as plain text
func (ctx *Context) PlainText(status int, bs []byte) {
ctx.Resp.WriteHeader(status)
ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8")
if _, err := ctx.Resp.Write(bs); err != nil {
ctx.ServerError("Write bytes failed", err)
}
}

// ServeFile serves given file to response.
func (ctx *Context) ServeFile(file string, names ...string) {
var name string
if len(names) > 0 {
name = names[0]
} else {
name = path.Base(file)
}
ctx.Resp.Header().Set("Content-Description", "File Transfer")
ctx.Resp.Header().Set("Content-Type", "application/octet-stream")
ctx.Resp.Header().Set("Content-Disposition", "attachment; filename="+name)
ctx.Resp.Header().Set("Content-Transfer-Encoding", "binary")
ctx.Resp.Header().Set("Expires", "0")
ctx.Resp.Header().Set("Cache-Control", "must-revalidate")
ctx.Resp.Header().Set("Pragma", "public")
http.ServeFile(ctx.Resp, ctx.Req, file)
}

// ServeStream serves file via io stream
func (ctx *Context) ServeStream(rd io.Reader, name string) {
ctx.Resp.Header().Set("Content-Description", "File Transfer")
Expand All @@ -368,34 +331,6 @@ func (ctx *Context) ServeStream(rd io.Reader, name string) {
}
}

// Error returned an error to web browser
func (ctx *Context) Error(status int, contents ...string) {
var v = http.StatusText(status)
if len(contents) > 0 {
v = contents[0]
}
http.Error(ctx.Resp, v, status)
}

// JSON render content as JSON
func (ctx *Context) JSON(status int, content interface{}) {
ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8")
ctx.Resp.WriteHeader(status)
if err := json.NewEncoder(ctx.Resp).Encode(content); err != nil {
ctx.ServerError("Render JSON failed", err)
}
}

// Redirect redirect the request
func (ctx *Context) Redirect(location string, status ...int) {
code := http.StatusFound
if len(status) == 1 {
code = status[0]
}

http.Redirect(ctx.Resp, ctx.Req, location, code)
}

// SetCookie convenience function to set most cookies consistently
// CSRF and a few others are the exception here
func (ctx *Context) SetCookie(name, value string, expiry int) {
Expand Down Expand Up @@ -483,64 +418,6 @@ func (ctx *Context) GetCookieFloat64(name string) float64 {
return v
}

// RemoteAddr returns the client machie ip address
func (ctx *Context) RemoteAddr() string {
return ctx.Req.RemoteAddr
}

// Params returns the param on route
func (ctx *Context) Params(p string) string {
s, _ := url.PathUnescape(chi.URLParam(ctx.Req, strings.TrimPrefix(p, ":")))
return s
}

// ParamsInt64 returns the param on route as int64
func (ctx *Context) ParamsInt64(p string) int64 {
v, _ := strconv.ParseInt(ctx.Params(p), 10, 64)
return v
}

// SetParams set params into routes
func (ctx *Context) SetParams(k, v string) {
chiCtx := chi.RouteContext(ctx)
chiCtx.URLParams.Add(strings.TrimPrefix(k, ":"), url.PathEscape(v))
}

// Write writes data to webbrowser
func (ctx *Context) Write(bs []byte) (int, error) {
return ctx.Resp.Write(bs)
}

// Written returns true if there are something sent to web browser
func (ctx *Context) Written() bool {
return ctx.Resp.Status() > 0
}

// Status writes status code
func (ctx *Context) Status(status int) {
ctx.Resp.WriteHeader(status)
}

// Deadline is part of the interface for context.Context and we pass this to the request context
func (ctx *Context) Deadline() (deadline time.Time, ok bool) {
return ctx.Req.Context().Deadline()
}

// Done is part of the interface for context.Context and we pass this to the request context
func (ctx *Context) Done() <-chan struct{} {
return ctx.Req.Context().Done()
}

// Err is part of the interface for context.Context and we pass this to the request context
func (ctx *Context) Err() error {
return ctx.Req.Context().Err()
}

// Value is part of the interface for context.Context and we pass this to the request context
func (ctx *Context) Value(key interface{}) interface{} {
return ctx.Req.Context().Value(key)
}

// Handler represents a custom handler
type Handler func(*Context)

Expand Down Expand Up @@ -645,7 +522,12 @@ func Contexter() func(next http.Handler) http.Handler {
var startTime = time.Now()
var link = setting.AppSubURL + strings.TrimSuffix(req.URL.EscapedPath(), "/")
var ctx = Context{
Resp: NewResponse(resp),
BaseContext: NewBaseContext(resp, req, map[string]interface{}{
"CurrentURL": setting.AppSubURL + req.URL.RequestURI(),
"PageStartTime": startTime,
"Link": link,
"RunModeIsProd": setting.IsProd,
}),
Cache: mc.GetCache(),
Locale: locale,
Link: link,
Expand All @@ -655,12 +537,6 @@ func Contexter() func(next http.Handler) http.Handler {
PullRequest: &PullRequest{},
},
Org: &Organization{},
Data: map[string]interface{}{
"CurrentURL": setting.AppSubURL + req.URL.RequestURI(),
"PageStartTime": startTime,
"Link": link,
"RunModeIsProd": setting.IsProd,
},
}
// PageData is passed by reference, and it will be rendered to `window.config.pageData` in `head.tmpl` for JavaScript modules
ctx.PageData = map[string]interface{}{}
Expand Down
Loading