diff options
Diffstat (limited to 'routers/common')
-rw-r--r-- | routers/common/actions.go | 71 | ||||
-rw-r--r-- | routers/common/blockexpensive.go | 90 | ||||
-rw-r--r-- | routers/common/blockexpensive_test.go | 30 | ||||
-rw-r--r-- | routers/common/codesearch.go | 30 | ||||
-rw-r--r-- | routers/common/db.go | 11 | ||||
-rw-r--r-- | routers/common/errpage.go | 2 | ||||
-rw-r--r-- | routers/common/errpage_test.go | 3 | ||||
-rw-r--r-- | routers/common/markup.go | 14 | ||||
-rw-r--r-- | routers/common/middleware.go | 27 | ||||
-rw-r--r-- | routers/common/pagetmpl.go | 75 | ||||
-rw-r--r-- | routers/common/qos.go | 145 | ||||
-rw-r--r-- | routers/common/qos_test.go | 91 | ||||
-rw-r--r-- | routers/common/serve.go | 17 |
13 files changed, 564 insertions, 42 deletions
diff --git a/routers/common/actions.go b/routers/common/actions.go new file mode 100644 index 0000000000..a4eabb6ba2 --- /dev/null +++ b/routers/common/actions.go @@ -0,0 +1,71 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "fmt" + "strings" + + actions_model "code.gitea.io/gitea/models/actions" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" +) + +func DownloadActionsRunJobLogsWithIndex(ctx *context.Base, ctxRepo *repo_model.Repository, runID, jobIndex int64) error { + runJobs, err := actions_model.GetRunJobsByRunID(ctx, runID) + if err != nil { + return fmt.Errorf("GetRunJobsByRunID: %w", err) + } + if err = runJobs.LoadRepos(ctx); err != nil { + return fmt.Errorf("LoadRepos: %w", err) + } + if jobIndex < 0 || jobIndex >= int64(len(runJobs)) { + return util.NewNotExistErrorf("job index is out of range: %d", jobIndex) + } + return DownloadActionsRunJobLogs(ctx, ctxRepo, runJobs[jobIndex]) +} + +func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository, curJob *actions_model.ActionRunJob) error { + if curJob.Repo.ID != ctxRepo.ID { + return util.NewNotExistErrorf("job not found") + } + + if curJob.TaskID == 0 { + return util.NewNotExistErrorf("job not started") + } + + if err := curJob.LoadRun(ctx); err != nil { + return fmt.Errorf("LoadRun: %w", err) + } + + task, err := actions_model.GetTaskByID(ctx, curJob.TaskID) + if err != nil { + return fmt.Errorf("GetTaskByID: %w", err) + } + + if task.LogExpired { + return util.NewNotExistErrorf("logs have been cleaned up") + } + + reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename) + if err != nil { + return fmt.Errorf("OpenLogs: %w", err) + } + defer reader.Close() + + workflowName := curJob.Run.WorkflowID + if p := strings.Index(workflowName, "."); p > 0 { + workflowName = workflowName[0:p] + } + ctx.ServeContent(reader, &context.ServeHeaderOptions{ + Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, curJob.Name, task.ID), + ContentLength: &task.LogSize, + ContentType: "text/plain", + ContentTypeCharset: "utf-8", + Disposition: "attachment", + }) + return nil +} diff --git a/routers/common/blockexpensive.go b/routers/common/blockexpensive.go new file mode 100644 index 0000000000..f52aa2b709 --- /dev/null +++ b/routers/common/blockexpensive.go @@ -0,0 +1,90 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "net/http" + "strings" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/reqctx" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/web/middleware" + + "github.com/go-chi/chi/v5" +) + +func BlockExpensive() func(next http.Handler) http.Handler { + if !setting.Service.BlockAnonymousAccessExpensive { + return nil + } + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + ret := determineRequestPriority(reqctx.FromContext(req.Context())) + if !ret.SignedIn { + if ret.Expensive || ret.LongPolling { + http.Redirect(w, req, setting.AppSubURL+"/user/login", http.StatusSeeOther) + return + } + } + next.ServeHTTP(w, req) + }) + } +} + +func isRoutePathExpensive(routePattern string) bool { + if strings.HasPrefix(routePattern, "/user/") || strings.HasPrefix(routePattern, "/login/") { + return false + } + + expensivePaths := []string{ + // code related + "/{username}/{reponame}/archive/", + "/{username}/{reponame}/blame/", + "/{username}/{reponame}/commit/", + "/{username}/{reponame}/commits/", + "/{username}/{reponame}/graph", + "/{username}/{reponame}/media/", + "/{username}/{reponame}/raw/", + "/{username}/{reponame}/src/", + + // issue & PR related (no trailing slash) + "/{username}/{reponame}/issues", + "/{username}/{reponame}/{type:issues}", + "/{username}/{reponame}/pulls", + "/{username}/{reponame}/{type:pulls}", + + // wiki + "/{username}/{reponame}/wiki/", + + // activity + "/{username}/{reponame}/activity/", + } + for _, path := range expensivePaths { + if strings.HasPrefix(routePattern, path) { + return true + } + } + return false +} + +func isRoutePathForLongPolling(routePattern string) bool { + return routePattern == "/user/events" +} + +func determineRequestPriority(reqCtx reqctx.RequestContext) (ret struct { + SignedIn bool + Expensive bool + LongPolling bool +}, +) { + chiRoutePath := chi.RouteContext(reqCtx).RoutePattern() + if _, ok := reqCtx.GetData()[middleware.ContextDataKeySignedUser].(*user_model.User); ok { + ret.SignedIn = true + } else { + ret.Expensive = isRoutePathExpensive(chiRoutePath) + ret.LongPolling = isRoutePathForLongPolling(chiRoutePath) + } + return ret +} diff --git a/routers/common/blockexpensive_test.go b/routers/common/blockexpensive_test.go new file mode 100644 index 0000000000..db5c0db7dd --- /dev/null +++ b/routers/common/blockexpensive_test.go @@ -0,0 +1,30 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBlockExpensive(t *testing.T) { + cases := []struct { + expensive bool + routePath string + }{ + {false, "/user/xxx"}, + {false, "/login/xxx"}, + {true, "/{username}/{reponame}/archive/xxx"}, + {true, "/{username}/{reponame}/graph"}, + {true, "/{username}/{reponame}/src/xxx"}, + {true, "/{username}/{reponame}/wiki/xxx"}, + {true, "/{username}/{reponame}/activity/xxx"}, + } + for _, c := range cases { + assert.Equal(t, c.expensive, isRoutePathExpensive(c.routePath), "routePath: %s", c.routePath) + } + + assert.True(t, isRoutePathForLongPolling("/user/events")) +} diff --git a/routers/common/codesearch.go b/routers/common/codesearch.go index a14af126e5..9bec448d7e 100644 --- a/routers/common/codesearch.go +++ b/routers/common/codesearch.go @@ -4,36 +4,30 @@ package common import ( + "code.gitea.io/gitea/modules/indexer" + code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/context" ) func PrepareCodeSearch(ctx *context.Context) (ret struct { - Keyword string - Language string - IsFuzzy bool + Keyword string + Language string + SearchMode indexer.SearchModeType }, ) { ret.Language = ctx.FormTrim("l") ret.Keyword = ctx.FormTrim("q") + ret.SearchMode = indexer.SearchModeType(ctx.FormTrim("search_mode")) - fuzzyDefault := setting.Indexer.RepoIndexerEnabled - fuzzyAllow := true - if setting.Indexer.RepoType == "bleve" && setting.Indexer.TypeBleveMaxFuzzniess == 0 { - fuzzyDefault = false - fuzzyAllow = false - } - isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(fuzzyDefault) - if isFuzzy && !fuzzyAllow { - ctx.Flash.Info("Fuzzy search is disabled by default due to performance reasons") - isFuzzy = false - } - - ctx.Data["IsBleveFuzzyDisabled"] = true ctx.Data["Keyword"] = ret.Keyword ctx.Data["Language"] = ret.Language - ctx.Data["IsFuzzy"] = isFuzzy - + ctx.Data["SelectedSearchMode"] = string(ret.SearchMode) + if setting.Indexer.RepoIndexerEnabled { + ctx.Data["SearchModes"] = code_indexer.SupportedSearchModes() + } else { + ctx.Data["SearchModes"] = indexer.GitGrepSupportedSearchModes() + } ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled return ret } diff --git a/routers/common/db.go b/routers/common/db.go index 61b331760c..01c0261427 100644 --- a/routers/common/db.go +++ b/routers/common/db.go @@ -5,7 +5,7 @@ package common import ( "context" - "fmt" + "errors" "time" "code.gitea.io/gitea/models/db" @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting/config" + "code.gitea.io/gitea/services/versioned_migration" "xorm.io/xorm" ) @@ -24,7 +25,7 @@ func InitDBEngine(ctx context.Context) (err error) { for i := 0; i < setting.Database.DBConnectRetries; i++ { select { case <-ctx.Done(): - return fmt.Errorf("Aborted due to shutdown:\nin retry ORM engine initialization") + return errors.New("Aborted due to shutdown:\nin retry ORM engine initialization") default: } log.Info("ORM engine initialization attempt #%d/%d...", i+1, setting.Database.DBConnectRetries) @@ -41,16 +42,16 @@ func InitDBEngine(ctx context.Context) (err error) { return nil } -func migrateWithSetting(x *xorm.Engine) error { +func migrateWithSetting(ctx context.Context, x *xorm.Engine) error { if setting.Database.AutoMigration { - return migrations.Migrate(x) + return versioned_migration.Migrate(ctx, x) } if current, err := migrations.GetCurrentDBVersion(x); err != nil { return err } else if current < 0 { // execute migrations when the database isn't initialized even if AutoMigration is false - return migrations.Migrate(x) + return versioned_migration.Migrate(ctx, x) } else if expected := migrations.ExpectedDBVersion(); current != expected { log.Fatal(`"database.AUTO_MIGRATION" is disabled, but current database version %d is not equal to the expected version %d.`+ `You can set "database.AUTO_MIGRATION" to true or migrate manually by running "gitea [--config /path/to/app.ini] migrate"`, current, expected) diff --git a/routers/common/errpage.go b/routers/common/errpage.go index c0b16dbdde..9ca309931b 100644 --- a/routers/common/errpage.go +++ b/routers/common/errpage.go @@ -32,7 +32,7 @@ func RenderPanicErrorPage(w http.ResponseWriter, req *http.Request, err any) { routing.UpdatePanicError(req.Context(), err) - httpcache.SetCacheControlInHeader(w.Header(), 0, "no-transform") + httpcache.SetCacheControlInHeader(w.Header(), &httpcache.CacheControlOptions{NoTransform: true}) w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions) tmplCtx := context.TemplateContext{} diff --git a/routers/common/errpage_test.go b/routers/common/errpage_test.go index dfea55f510..33aa6bb339 100644 --- a/routers/common/errpage_test.go +++ b/routers/common/errpage_test.go @@ -4,7 +4,6 @@ package common import ( - "context" "errors" "net/http" "net/http/httptest" @@ -21,7 +20,7 @@ import ( func TestRenderPanicErrorPage(t *testing.T) { w := httptest.NewRecorder() req := &http.Request{URL: &url.URL{}} - req = req.WithContext(reqctx.NewRequestContextForTest(context.Background())) + req = req.WithContext(reqctx.NewRequestContextForTest(t.Context())) RenderPanicErrorPage(w, req, errors.New("fake panic error (for test only)")) respContent := w.Body.String() assert.Contains(t, respContent, `class="page-content status-page-500"`) diff --git a/routers/common/markup.go b/routers/common/markup.go index 533b546a2a..00b2dd07c6 100644 --- a/routers/common/markup.go +++ b/routers/common/markup.go @@ -39,7 +39,7 @@ func RenderMarkup(ctx *context.Base, ctxRepo *context.Repository, mode, text, ur rctx := renderhelper.NewRenderContextSimpleDocument(ctx, baseLink).WithUseAbsoluteLink(true). WithMarkupType(markdown.MarkupName) if err := markdown.RenderRaw(rctx, strings.NewReader(text), ctx.Resp); err != nil { - ctx.Error(http.StatusInternalServerError, err.Error()) + ctx.HTTPError(http.StatusInternalServerError, err.Error()) } return } @@ -76,7 +76,11 @@ func RenderMarkup(ctx *context.Base, ctxRepo *context.Repository, mode, text, ur }) rctx = rctx.WithMarkupType(markdown.MarkupName) case "comment": - rctx = renderhelper.NewRenderContextRepoComment(ctx, repoModel, renderhelper.RepoCommentOptions{DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName}) + rctx = renderhelper.NewRenderContextRepoComment(ctx, repoModel, renderhelper.RepoCommentOptions{ + DeprecatedOwnerName: repoOwnerName, + DeprecatedRepoName: repoName, + FootnoteContextID: "preview", + }) rctx = rctx.WithMarkupType(markdown.MarkupName) case "wiki": rctx = renderhelper.NewRenderContextRepoWiki(ctx, repoModel, renderhelper.RepoWikiOptions{DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName}) @@ -88,15 +92,15 @@ func RenderMarkup(ctx *context.Base, ctxRepo *context.Repository, mode, text, ur }) rctx = rctx.WithMarkupType("").WithRelativePath(filePath) // render the repo file content by its extension default: - ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Unknown mode: %s", mode)) + ctx.HTTPError(http.StatusUnprocessableEntity, "Unknown mode: "+mode) return } rctx = rctx.WithUseAbsoluteLink(true) if err := markup.Render(rctx, strings.NewReader(text), ctx.Resp); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.Error(http.StatusUnprocessableEntity, err.Error()) + ctx.HTTPError(http.StatusUnprocessableEntity, err.Error()) } else { - ctx.Error(http.StatusInternalServerError, err.Error()) + ctx.HTTPError(http.StatusInternalServerError, err.Error()) } return } diff --git a/routers/common/middleware.go b/routers/common/middleware.go index 12b0c67b01..2ba02de8ed 100644 --- a/routers/common/middleware.go +++ b/routers/common/middleware.go @@ -9,6 +9,7 @@ import ( "strings" "code.gitea.io/gitea/modules/cache" + "code.gitea.io/gitea/modules/gtprof" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" @@ -43,14 +44,26 @@ func ProtocolMiddlewares() (handlers []any) { func RequestContextHandler() func(h http.Handler) http.Handler { return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - profDesc := fmt.Sprintf("%s: %s", req.Method, req.RequestURI) + return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) { + // this response writer might not be the same as the one in context.Base.Resp + // because there might be a "gzip writer" in the middle, so the "written size" here is the compressed size + respWriter := context.WrapResponseWriter(respOrig) + + profDesc := fmt.Sprintf("HTTP: %s %s", req.Method, req.RequestURI) ctx, finished := reqctx.NewRequestContext(req.Context(), profDesc) defer finished() + ctx, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanHTTP) + req = req.WithContext(ctx) + defer func() { + chiCtx := chi.RouteContext(req.Context()) + span.SetAttributeString(gtprof.TraceAttrHTTPRoute, chiCtx.RoutePattern()) + span.End() + }() + defer func() { if err := recover(); err != nil { - RenderPanicErrorPage(resp, req, err) // it should never panic + RenderPanicErrorPage(respWriter, req, err) // it should never panic } }() @@ -62,7 +75,7 @@ func RequestContextHandler() func(h http.Handler) http.Handler { _ = req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory } }) - next.ServeHTTP(context.WrapResponseWriter(resp), req) + next.ServeHTTP(respWriter, req) }) } } @@ -71,11 +84,11 @@ func ChiRoutePathHandler() func(h http.Handler) http.Handler { // make sure chi uses EscapedPath(RawPath) as RoutePath, then "%2f" could be handled correctly return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - ctx := chi.RouteContext(req.Context()) + chiCtx := chi.RouteContext(req.Context()) if req.URL.RawPath == "" { - ctx.RoutePath = req.URL.EscapedPath() + chiCtx.RoutePath = req.URL.EscapedPath() } else { - ctx.RoutePath = req.URL.RawPath + chiCtx.RoutePath = req.URL.RawPath } next.ServeHTTP(resp, req) }) diff --git a/routers/common/pagetmpl.go b/routers/common/pagetmpl.go new file mode 100644 index 0000000000..52c9fceba3 --- /dev/null +++ b/routers/common/pagetmpl.go @@ -0,0 +1,75 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + goctx "context" + "errors" + + activities_model "code.gitea.io/gitea/models/activities" + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/services/context" +) + +// StopwatchTmplInfo is a view on a stopwatch specifically for template rendering +type StopwatchTmplInfo struct { + IssueLink string + RepoSlug string + IssueIndex int64 + Seconds int64 +} + +func getActiveStopwatch(goCtx goctx.Context) *StopwatchTmplInfo { + ctx := context.GetWebContext(goCtx) + if ctx.Doer == nil { + return nil + } + + _, sw, issue, err := issues_model.HasUserStopwatch(ctx, ctx.Doer.ID) + if err != nil { + if !errors.Is(err, goctx.Canceled) { + log.Error("Unable to HasUserStopwatch for user:%-v: %v", ctx.Doer, err) + } + return nil + } + + if sw == nil || sw.ID == 0 { + return nil + } + + return &StopwatchTmplInfo{ + issue.Link(), + issue.Repo.FullName(), + issue.Index, + sw.Seconds() + 1, // ensure time is never zero in ui + } +} + +func notificationUnreadCount(goCtx goctx.Context) int64 { + ctx := context.GetWebContext(goCtx) + if ctx.Doer == nil { + return 0 + } + count, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{ + UserID: ctx.Doer.ID, + Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread}, + }) + if err != nil { + if !errors.Is(err, goctx.Canceled) { + log.Error("Unable to find notification for user:%-v: %v", ctx.Doer, err) + } + return 0 + } + return count +} + +func PageTmplFunctions(ctx *context.Context) { + if ctx.IsSigned { + // defer the function call to the last moment when the tmpl renders + ctx.Data["NotificationUnreadCount"] = notificationUnreadCount + ctx.Data["GetActiveStopwatch"] = getActiveStopwatch + } +} diff --git a/routers/common/qos.go b/routers/common/qos.go new file mode 100644 index 0000000000..e50fbe4f69 --- /dev/null +++ b/routers/common/qos.go @@ -0,0 +1,145 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "context" + "fmt" + "net/http" + "strings" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/web/middleware" + giteacontext "code.gitea.io/gitea/services/context" + + "github.com/bohde/codel" + "github.com/go-chi/chi/v5" +) + +const tplStatus503 templates.TplName = "status/503" + +type Priority int + +func (p Priority) String() string { + switch p { + case HighPriority: + return "high" + case DefaultPriority: + return "default" + case LowPriority: + return "low" + default: + return fmt.Sprintf("%d", p) + } +} + +const ( + LowPriority = Priority(-10) + DefaultPriority = Priority(0) + HighPriority = Priority(10) +) + +// QoS implements quality of service for requests, based upon whether +// or not the user is logged in. All traffic may get dropped, and +// anonymous users are deprioritized. +func QoS() func(next http.Handler) http.Handler { + if !setting.Service.QoS.Enabled { + return nil + } + + maxOutstanding := setting.Service.QoS.MaxInFlightRequests + if maxOutstanding <= 0 { + maxOutstanding = 10 + } + + c := codel.NewPriority(codel.Options{ + // The maximum number of waiting requests. + MaxPending: setting.Service.QoS.MaxWaitingRequests, + // The maximum number of in-flight requests. + MaxOutstanding: maxOutstanding, + // The target latency that a blocked request should wait + // for. After this, it might be dropped. + TargetLatency: setting.Service.QoS.TargetWaitTime, + }) + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + + priority := requestPriority(ctx) + + // Check if the request can begin processing. + err := c.Acquire(ctx, int(priority)) + if err != nil { + log.Error("QoS error, dropping request of priority %s: %v", priority, err) + renderServiceUnavailable(w, req) + return + } + + // Release long-polling immediately, so they don't always + // take up an in-flight request + if strings.Contains(req.URL.Path, "/user/events") { + c.Release() + } else { + defer c.Release() + } + + next.ServeHTTP(w, req) + }) + } +} + +// requestPriority assigns a priority value for a request based upon +// whether the user is logged in and how expensive the endpoint is +func requestPriority(ctx context.Context) Priority { + // If the user is logged in, assign high priority. + data := middleware.GetContextData(ctx) + if _, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok { + return HighPriority + } + + rctx := chi.RouteContext(ctx) + if rctx == nil { + return DefaultPriority + } + + // If we're operating in the context of a repo, assign low priority + routePattern := rctx.RoutePattern() + if strings.HasPrefix(routePattern, "/{username}/{reponame}/") { + return LowPriority + } + + return DefaultPriority +} + +// renderServiceUnavailable will render an HTTP 503 Service +// Unavailable page, providing HTML if the client accepts it. +func renderServiceUnavailable(w http.ResponseWriter, req *http.Request) { + acceptsHTML := false + for _, part := range req.Header["Accept"] { + if strings.Contains(part, "text/html") { + acceptsHTML = true + break + } + } + + // If the client doesn't accept HTML, then render a plain text response + if !acceptsHTML { + http.Error(w, "503 Service Unavailable", http.StatusServiceUnavailable) + return + } + + tmplCtx := giteacontext.TemplateContext{} + tmplCtx["Locale"] = middleware.Locale(w, req) + ctxData := middleware.GetContextData(req.Context()) + err := templates.HTMLRenderer().HTML(w, http.StatusServiceUnavailable, tplStatus503, ctxData, tmplCtx) + if err != nil { + log.Error("Error occurs again when rendering service unavailable page: %v", err) + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("Internal server error, please collect error logs and report to Gitea issue tracker")) + } +} diff --git a/routers/common/qos_test.go b/routers/common/qos_test.go new file mode 100644 index 0000000000..850a5f51db --- /dev/null +++ b/routers/common/qos_test.go @@ -0,0 +1,91 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "net/http" + "testing" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/services/contexttest" + + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/assert" +) + +func TestRequestPriority(t *testing.T) { + type test struct { + Name string + User *user_model.User + RoutePattern string + Expected Priority + } + + cases := []test{ + { + Name: "Logged In", + User: &user_model.User{}, + Expected: HighPriority, + }, + { + Name: "Sign In", + RoutePattern: "/user/login", + Expected: DefaultPriority, + }, + { + Name: "Repo Home", + RoutePattern: "/{username}/{reponame}", + Expected: DefaultPriority, + }, + { + Name: "User Repo", + RoutePattern: "/{username}/{reponame}/src/branch/main", + Expected: LowPriority, + }, + } + + for _, tc := range cases { + t.Run(tc.Name, func(t *testing.T) { + ctx, _ := contexttest.MockContext(t, "") + + if tc.User != nil { + data := middleware.GetContextData(ctx) + data[middleware.ContextDataKeySignedUser] = tc.User + } + + rctx := chi.RouteContext(ctx) + rctx.RoutePatterns = []string{tc.RoutePattern} + + assert.Exactly(t, tc.Expected, requestPriority(ctx)) + }) + } +} + +func TestRenderServiceUnavailable(t *testing.T) { + t.Run("HTML", func(t *testing.T) { + ctx, resp := contexttest.MockContext(t, "") + ctx.Req.Header.Set("Accept", "text/html") + + renderServiceUnavailable(resp, ctx.Req) + assert.Equal(t, http.StatusServiceUnavailable, resp.Code) + assert.Contains(t, resp.Header().Get("Content-Type"), "text/html") + + body := resp.Body.String() + assert.Contains(t, body, `lang="en-US"`) + assert.Contains(t, body, "503 Service Unavailable") + }) + + t.Run("plain", func(t *testing.T) { + ctx, resp := contexttest.MockContext(t, "") + ctx.Req.Header.Set("Accept", "text/plain") + + renderServiceUnavailable(resp, ctx.Req) + assert.Equal(t, http.StatusServiceUnavailable, resp.Code) + assert.Contains(t, resp.Header().Get("Content-Type"), "text/plain") + + body := resp.Body.String() + assert.Contains(t, body, "503 Service Unavailable") + }) +} diff --git a/routers/common/serve.go b/routers/common/serve.go index 446908db75..862230b30f 100644 --- a/routers/common/serve.go +++ b/routers/common/serve.go @@ -5,17 +5,21 @@ package common import ( "io" + "path" "time" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/services/context" ) // ServeBlob download a git.Blob -func ServeBlob(ctx *context.Base, filePath string, blob *git.Blob, lastModified *time.Time) error { +func ServeBlob(ctx *context.Base, repo *repo_model.Repository, filePath string, blob *git.Blob, lastModified *time.Time) error { if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) { return nil } @@ -30,14 +34,19 @@ func ServeBlob(ctx *context.Base, filePath string, blob *git.Blob, lastModified } }() - httplib.ServeContentByReader(ctx.Req, ctx.Resp, filePath, blob.Size(), dataRc) + _ = repo.LoadOwner(ctx) + httplib.ServeContentByReader(ctx.Req, ctx.Resp, blob.Size(), dataRc, &httplib.ServeHeaderOptions{ + Filename: path.Base(filePath), + CacheIsPublic: !repo.IsPrivate && repo.Owner != nil && repo.Owner.Visibility == structs.VisibleTypePublic, + CacheDuration: setting.StaticCacheTime, + }) return nil } func ServeContentByReader(ctx *context.Base, filePath string, size int64, reader io.Reader) { - httplib.ServeContentByReader(ctx.Req, ctx.Resp, filePath, size, reader) + httplib.ServeContentByReader(ctx.Req, ctx.Resp, size, reader, &httplib.ServeHeaderOptions{Filename: path.Base(filePath)}) } func ServeContentByReadSeeker(ctx *context.Base, filePath string, modTime *time.Time, reader io.ReadSeeker) { - httplib.ServeContentByReadSeeker(ctx.Req, ctx.Resp, filePath, modTime, reader) + httplib.ServeContentByReadSeeker(ctx.Req, ctx.Resp, modTime, reader, &httplib.ServeHeaderOptions{Filename: path.Base(filePath)}) } |