diff options
author | wxiaoguang <wxiaoguang@gmail.com> | 2024-12-28 11:31:46 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-12-28 03:31:46 +0000 |
commit | e435b1900a00dd98d65aa1f7668fe41e4df83044 (patch) | |
tree | dc8156c59b3b92589f79a29610935ef9d12516a7 | |
parent | 254314be5f355ab5dd5533fde6e9d4f62f1620a4 (diff) | |
download | gitea-e435b1900a00dd98d65aa1f7668fe41e4df83044.tar.gz gitea-e435b1900a00dd98d65aa1f7668fe41e4df83044.zip |
Refactor arch route handlers (#32993)
-rw-r--r-- | modules/web/router.go (renamed from modules/web/route.go) | 153 | ||||
-rw-r--r-- | modules/web/router_combo.go | 41 | ||||
-rw-r--r-- | modules/web/router_path.go | 135 | ||||
-rw-r--r-- | modules/web/router_test.go (renamed from modules/web/route_test.go) | 82 | ||||
-rw-r--r-- | routers/api/packages/api.go | 39 |
5 files changed, 260 insertions, 190 deletions
diff --git a/modules/web/route.go b/modules/web/router.go index 729ac46cdc..da06b955b1 100644 --- a/modules/web/route.go +++ b/modules/web/router.go @@ -4,18 +4,14 @@ package web import ( - "fmt" "net/http" "net/url" "reflect" - "regexp" "strings" - "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web/middleware" "gitea.com/go-chi/binding" @@ -45,7 +41,7 @@ func GetForm(dataStore reqctx.RequestDataStore) any { // Router defines a route based on chi's router type Router struct { - chiRouter chi.Router + chiRouter *chi.Mux curGroupPrefix string curMiddlewares []any } @@ -97,16 +93,21 @@ func isNilOrFuncNil(v any) bool { return r.Kind() == reflect.Func && r.IsNil() } -func (r *Router) wrapMiddlewareAndHandler(h []any) ([]func(http.Handler) http.Handler, http.HandlerFunc) { - handlerProviders := make([]func(http.Handler) http.Handler, 0, len(r.curMiddlewares)+len(h)+1) - for _, m := range r.curMiddlewares { +func wrapMiddlewareAndHandler(curMiddlewares, h []any) ([]func(http.Handler) http.Handler, http.HandlerFunc) { + handlerProviders := make([]func(http.Handler) http.Handler, 0, len(curMiddlewares)+len(h)+1) + for _, m := range curMiddlewares { if !isNilOrFuncNil(m) { handlerProviders = append(handlerProviders, toHandlerProvider(m)) } } - for _, m := range h { + if len(h) == 0 { + panic("no endpoint handler provided") + } + for i, m := range h { if !isNilOrFuncNil(m) { handlerProviders = append(handlerProviders, toHandlerProvider(m)) + } else if i == len(h)-1 { + panic("endpoint handler can't be nil") } } middlewares := handlerProviders[:len(handlerProviders)-1] @@ -121,7 +122,7 @@ func (r *Router) wrapMiddlewareAndHandler(h []any) ([]func(http.Handler) http.Ha // Methods adds the same handlers for multiple http "methods" (separated by ","). // If any method is invalid, the lower level router will panic. func (r *Router) Methods(methods, pattern string, h ...any) { - middlewares, handlerFunc := r.wrapMiddlewareAndHandler(h) + middlewares, handlerFunc := wrapMiddlewareAndHandler(r.curMiddlewares, h) fullPattern := r.getPattern(pattern) if strings.Contains(methods, ",") { methods := strings.Split(methods, ",") @@ -141,7 +142,7 @@ func (r *Router) Mount(pattern string, subRouter *Router) { // Any delegate requests for all methods func (r *Router) Any(pattern string, h ...any) { - middlewares, handlerFunc := r.wrapMiddlewareAndHandler(h) + middlewares, handlerFunc := wrapMiddlewareAndHandler(r.curMiddlewares, h) r.chiRouter.With(middlewares...).HandleFunc(r.getPattern(pattern), handlerFunc) } @@ -185,17 +186,6 @@ func (r *Router) NotFound(h http.HandlerFunc) { r.chiRouter.NotFound(h) } -type pathProcessorParam struct { - name string - captureGroup int -} - -type PathProcessor struct { - methods container.Set[string] - re *regexp.Regexp - params []pathProcessorParam -} - func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Request, next http.Handler) { normalized := false normalizedPath := req.URL.EscapedPath() @@ -253,121 +243,16 @@ func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Reques next.ServeHTTP(resp, req) } -func (p *PathProcessor) ProcessRequestPath(chiCtx *chi.Context, path string) bool { - if !p.methods.Contains(chiCtx.RouteMethod) { - return false - } - if !strings.HasPrefix(path, "/") { - path = "/" + path - } - pathMatches := p.re.FindStringSubmatchIndex(path) // Golang regexp match pairs [start, end, start, end, ...] - if pathMatches == nil { - return false - } - var paramMatches [][]int - for i := 2; i < len(pathMatches); { - paramMatches = append(paramMatches, []int{pathMatches[i], pathMatches[i+1]}) - pmIdx := len(paramMatches) - 1 - end := pathMatches[i+1] - i += 2 - for ; i < len(pathMatches); i += 2 { - if pathMatches[i] >= end { - break - } - paramMatches[pmIdx] = append(paramMatches[pmIdx], pathMatches[i], pathMatches[i+1]) - } - } - for i, pm := range paramMatches { - groupIdx := p.params[i].captureGroup * 2 - chiCtx.URLParams.Add(p.params[i].name, path[pm[groupIdx]:pm[groupIdx+1]]) - } - return true -} - -func NewPathProcessor(methods, pattern string) *PathProcessor { - p := &PathProcessor{methods: make(container.Set[string])} - for _, method := range strings.Split(methods, ",") { - p.methods.Add(strings.TrimSpace(method)) - } - re := []byte{'^'} - lastEnd := 0 - for lastEnd < len(pattern) { - start := strings.IndexByte(pattern[lastEnd:], '<') - if start == -1 { - re = append(re, pattern[lastEnd:]...) - break - } - end := strings.IndexByte(pattern[lastEnd+start:], '>') - if end == -1 { - panic(fmt.Sprintf("invalid pattern: %s", pattern)) - } - re = append(re, pattern[lastEnd:lastEnd+start]...) - partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":") - lastEnd += start + end + 1 - - // TODO: it could support to specify a "capture group" for the name, for example: "/<name[2]:(\d)-(\d)>" - // it is not used so no need to implement it now - param := pathProcessorParam{} - if partExp == "*" { - re = append(re, "(.*?)/?"...) - if lastEnd < len(pattern) { - if pattern[lastEnd] == '/' { - lastEnd++ - } - } - } else { - partExp = util.IfZero(partExp, "[^/]+") - re = append(re, '(') - re = append(re, partExp...) - re = append(re, ')') - } - param.name = partName - p.params = append(p.params, param) - } - re = append(re, '$') - reStr := string(re) - p.re = regexp.MustCompile(reStr) - return p -} - // Combo delegates requests to Combo func (r *Router) Combo(pattern string, h ...any) *Combo { return &Combo{r, pattern, h} } -// Combo represents a tiny group routes with same pattern -type Combo struct { - r *Router - pattern string - h []any -} - -// Get delegates Get method -func (c *Combo) Get(h ...any) *Combo { - c.r.Get(c.pattern, append(c.h, h...)...) - return c -} - -// Post delegates Post method -func (c *Combo) Post(h ...any) *Combo { - c.r.Post(c.pattern, append(c.h, h...)...) - return c -} - -// Delete delegates Delete method -func (c *Combo) Delete(h ...any) *Combo { - c.r.Delete(c.pattern, append(c.h, h...)...) - return c -} - -// Put delegates Put method -func (c *Combo) Put(h ...any) *Combo { - c.r.Put(c.pattern, append(c.h, h...)...) - return c -} - -// Patch delegates Patch method -func (c *Combo) Patch(h ...any) *Combo { - c.r.Patch(c.pattern, append(c.h, h...)...) - return c +// PathGroup creates a group of paths which could be matched by regexp. +// It is only designed to resolve some special cases which chi router can't handle. +// For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient). +func (r *Router) PathGroup(pattern string, fn func(g *RouterPathGroup), h ...any) { + g := &RouterPathGroup{r: r, pathParam: "*"} + fn(g) + r.Any(pattern, append(h, g.ServeHTTP)...) } diff --git a/modules/web/router_combo.go b/modules/web/router_combo.go new file mode 100644 index 0000000000..4478689027 --- /dev/null +++ b/modules/web/router_combo.go @@ -0,0 +1,41 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package web + +// Combo represents a tiny group routes with same pattern +type Combo struct { + r *Router + pattern string + h []any +} + +// Get delegates Get method +func (c *Combo) Get(h ...any) *Combo { + c.r.Get(c.pattern, append(c.h, h...)...) + return c +} + +// Post delegates Post method +func (c *Combo) Post(h ...any) *Combo { + c.r.Post(c.pattern, append(c.h, h...)...) + return c +} + +// Delete delegates Delete method +func (c *Combo) Delete(h ...any) *Combo { + c.r.Delete(c.pattern, append(c.h, h...)...) + return c +} + +// Put delegates Put method +func (c *Combo) Put(h ...any) *Combo { + c.r.Put(c.pattern, append(c.h, h...)...) + return c +} + +// Patch delegates Patch method +func (c *Combo) Patch(h ...any) *Combo { + c.r.Patch(c.pattern, append(c.h, h...)...) + return c +} diff --git a/modules/web/router_path.go b/modules/web/router_path.go new file mode 100644 index 0000000000..39082c0724 --- /dev/null +++ b/modules/web/router_path.go @@ -0,0 +1,135 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package web + +import ( + "fmt" + "net/http" + "regexp" + "strings" + + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/util" + + "github.com/go-chi/chi/v5" +) + +type RouterPathGroup struct { + r *Router + pathParam string + matchers []*routerPathMatcher +} + +func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + chiCtx := chi.RouteContext(req.Context()) + path := chiCtx.URLParam(g.pathParam) + for _, m := range g.matchers { + if m.matchPath(chiCtx, path) { + handler := m.handlerFunc + for i := len(m.middlewares) - 1; i >= 0; i-- { + handler = m.middlewares[i](handler).ServeHTTP + } + handler(resp, req) + return + } + } + g.r.chiRouter.NotFoundHandler().ServeHTTP(resp, req) +} + +// MatchPath matches the request method, and uses regexp to match the path. +// The pattern uses "<...>" to define path parameters, for example: "/<name>" (different from chi router) +// It is only designed to resolve some special cases which chi router can't handle. +// For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient). +func (g *RouterPathGroup) MatchPath(methods, pattern string, h ...any) { + g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...)) +} + +type routerPathParam struct { + name string + captureGroup int +} + +type routerPathMatcher struct { + methods container.Set[string] + re *regexp.Regexp + params []routerPathParam + middlewares []func(http.Handler) http.Handler + handlerFunc http.HandlerFunc +} + +func (p *routerPathMatcher) matchPath(chiCtx *chi.Context, path string) bool { + if !p.methods.Contains(chiCtx.RouteMethod) { + return false + } + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + pathMatches := p.re.FindStringSubmatchIndex(path) // Golang regexp match pairs [start, end, start, end, ...] + if pathMatches == nil { + return false + } + var paramMatches [][]int + for i := 2; i < len(pathMatches); { + paramMatches = append(paramMatches, []int{pathMatches[i], pathMatches[i+1]}) + pmIdx := len(paramMatches) - 1 + end := pathMatches[i+1] + i += 2 + for ; i < len(pathMatches); i += 2 { + if pathMatches[i] >= end { + break + } + paramMatches[pmIdx] = append(paramMatches[pmIdx], pathMatches[i], pathMatches[i+1]) + } + } + for i, pm := range paramMatches { + groupIdx := p.params[i].captureGroup * 2 + chiCtx.URLParams.Add(p.params[i].name, path[pm[groupIdx]:pm[groupIdx+1]]) + } + return true +} + +func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher { + middlewares, handlerFunc := wrapMiddlewareAndHandler(nil, h) + p := &routerPathMatcher{methods: make(container.Set[string]), middlewares: middlewares, handlerFunc: handlerFunc} + for _, method := range strings.Split(methods, ",") { + p.methods.Add(strings.TrimSpace(method)) + } + re := []byte{'^'} + lastEnd := 0 + for lastEnd < len(pattern) { + start := strings.IndexByte(pattern[lastEnd:], '<') + if start == -1 { + re = append(re, pattern[lastEnd:]...) + break + } + end := strings.IndexByte(pattern[lastEnd+start:], '>') + if end == -1 { + panic(fmt.Sprintf("invalid pattern: %s", pattern)) + } + re = append(re, pattern[lastEnd:lastEnd+start]...) + partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":") + lastEnd += start + end + 1 + + // TODO: it could support to specify a "capture group" for the name, for example: "/<name[2]:(\d)-(\d)>" + // it is not used so no need to implement it now + param := routerPathParam{} + if partExp == "*" { + re = append(re, "(.*?)/?"...) + if lastEnd < len(pattern) && pattern[lastEnd] == '/' { + lastEnd++ // the "*" pattern is able to handle the last slash, so skip it + } + } else { + partExp = util.IfZero(partExp, "[^/]+") + re = append(re, '(') + re = append(re, partExp...) + re = append(re, ')') + } + param.name = partName + p.params = append(p.params, param) + } + re = append(re, '$') + reStr := string(re) + p.re = regexp.MustCompile(reStr) + return p +} diff --git a/modules/web/route_test.go b/modules/web/router_test.go index ca3134b546..bdcf623b95 100644 --- a/modules/web/route_test.go +++ b/modules/web/router_test.go @@ -27,17 +27,21 @@ func chiURLParamsToMap(chiCtx *chi.Context) map[string]string { } m[key] = pathParams.Values[i] } - return m + return util.Iif(len(m) == 0, nil, m) } func TestPathProcessor(t *testing.T) { testProcess := func(pattern, uri string, expectedPathParams map[string]string) { chiCtx := chi.NewRouteContext() chiCtx.RouteMethod = "GET" - p := NewPathProcessor("GET", pattern) - assert.True(t, p.ProcessRequestPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri) + p := newRouterPathMatcher("GET", pattern, http.NotFound) + assert.True(t, p.matchPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri) assert.Equal(t, expectedPathParams, chiURLParamsToMap(chiCtx), "use pattern %s to process uri %s", pattern, uri) } + + // the "<...>" is intentionally designed to distinguish from chi's path parameters, because: + // 1. their behaviors are totally different, we do not want to mislead developers + // 2. we can write regexp in "<name:\w{3,4}>" easily and parse it easily testProcess("/<p1>/<p2>", "/a/b", map[string]string{"p1": "a", "p2": "b"}) testProcess("/<p1:*>", "", map[string]string{"p1": ""}) // this is a special case, because chi router could use empty path testProcess("/<p1:*>", "/", map[string]string{"p1": ""}) @@ -67,24 +71,31 @@ func TestRouter(t *testing.T) { } } + stopMark := func(optMark ...string) func(resp http.ResponseWriter, req *http.Request) { + mark := util.OptionalArg(optMark, "") + return func(resp http.ResponseWriter, req *http.Request) { + if stop := req.FormValue("stop"); stop != "" && (mark == "" || mark == stop) { + h(stop)(resp, req) + resp.WriteHeader(http.StatusOK) + } + } + } + r := NewRouter() + r.NotFound(h("not-found:/")) r.Get("/{username}/{reponame}/{type:issues|pulls}", h("list-issues-a")) // this one will never be called r.Group("/{username}/{reponame}", func() { r.Get("/{type:issues|pulls}", h("list-issues-b")) r.Group("", func() { r.Get("/{type:issues|pulls}/{index}", h("view-issue")) - }, func(resp http.ResponseWriter, req *http.Request) { - if stop := req.FormValue("stop"); stop != "" { - h(stop)(resp, req) - resp.WriteHeader(http.StatusOK) - } - }) + }, stopMark()) r.Group("/issues/{index}", func() { r.Post("/update", h("update-issue")) }) }) m := NewRouter() + m.NotFound(h("not-found:/api/v1")) r.Mount("/api/v1", m) m.Group("/repos", func() { m.Group("/{username}/{reponame}", func() { @@ -96,11 +107,14 @@ func TestRouter(t *testing.T) { m.Patch("", h()) m.Delete("", h()) }) + m.PathGroup("/*", func(g *RouterPathGroup) { + g.MatchPath("GET", `/<dir:*>/<file:[a-z]{1,2}>`, stopMark("s2"), h("match-path")) + }, stopMark("s1")) }) }) }) - testRoute := func(methodPath string, expected resultStruct) { + testRoute := func(t *testing.T, methodPath string, expected resultStruct) { t.Run(methodPath, func(t *testing.T) { res = resultStruct{} methodPathFields := strings.Fields(methodPath) @@ -111,24 +125,24 @@ func TestRouter(t *testing.T) { }) } - t.Run("Root Router", func(t *testing.T) { - testRoute("GET /the-user/the-repo/other", resultStruct{}) - testRoute("GET /the-user/the-repo/pulls", resultStruct{ + t.Run("RootRouter", func(t *testing.T) { + testRoute(t, "GET /the-user/the-repo/other", resultStruct{method: "GET", handlerMark: "not-found:/"}) + testRoute(t, "GET /the-user/the-repo/pulls", resultStruct{ method: "GET", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"}, handlerMark: "list-issues-b", }) - testRoute("GET /the-user/the-repo/issues/123", resultStruct{ + testRoute(t, "GET /the-user/the-repo/issues/123", resultStruct{ method: "GET", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, handlerMark: "view-issue", }) - testRoute("GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{ + testRoute(t, "GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{ method: "GET", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, handlerMark: "hijack", }) - testRoute("POST /the-user/the-repo/issues/123/update", resultStruct{ + testRoute(t, "POST /the-user/the-repo/issues/123/update", resultStruct{ method: "POST", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"}, handlerMark: "update-issue", @@ -136,31 +150,57 @@ func TestRouter(t *testing.T) { }) t.Run("Sub Router", func(t *testing.T) { - testRoute("GET /api/v1/repos/the-user/the-repo/branches", resultStruct{ + testRoute(t, "GET /api/v1/other", resultStruct{method: "GET", handlerMark: "not-found:/api/v1"}) + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches", resultStruct{ method: "GET", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"}, }) - testRoute("POST /api/v1/repos/the-user/the-repo/branches", resultStruct{ + testRoute(t, "POST /api/v1/repos/the-user/the-repo/branches", resultStruct{ method: "POST", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"}, }) - testRoute("GET /api/v1/repos/the-user/the-repo/branches/master", resultStruct{ + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/master", resultStruct{ method: "GET", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"}, }) - testRoute("PATCH /api/v1/repos/the-user/the-repo/branches/master", resultStruct{ + testRoute(t, "PATCH /api/v1/repos/the-user/the-repo/branches/master", resultStruct{ method: "PATCH", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"}, }) - testRoute("DELETE /api/v1/repos/the-user/the-repo/branches/master", resultStruct{ + testRoute(t, "DELETE /api/v1/repos/the-user/the-repo/branches/master", resultStruct{ method: "DELETE", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"}, }) }) + + t.Run("MatchPath", func(t *testing.T) { + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn", resultStruct{ + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, + handlerMark: "match-path", + }) + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/000", resultStruct{ + method: "GET", + pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"}, + handlerMark: "not-found:/api/v1", + }) + + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s1", resultStruct{ + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn"}, + handlerMark: "s1", + }) + + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s2", resultStruct{ + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, + handlerMark: "s2", + }) + }) } func TestRouteNormalizePath(t *testing.T) { diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index b50fbd638e..8c06836ff8 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -37,8 +37,6 @@ import ( "code.gitea.io/gitea/routers/api/packages/vagrant" "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/context" - - "github.com/go-chi/chi/v5" ) func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) { @@ -140,39 +138,10 @@ func CommonRoutes() *web.Router { }, reqPackageAccess(perm.AccessModeRead)) r.Group("/arch", func() { r.Methods("HEAD,GET", "/repository.key", arch.GetRepositoryKey) - - reqPutRepository := web.NewPathProcessor("PUT", "/<repository:*>") - reqGetRepoArchFile := web.NewPathProcessor("HEAD,GET", "/<repository:*>/<architecture>/<filename>") - reqDeleteRepoNameVerArch := web.NewPathProcessor("DELETE", "/<repository:*>/<name>/<version>/<architecture>") - - r.Any("*", func(ctx *context.Context) { - chiCtx := chi.RouteContext(ctx.Req.Context()) - path := ctx.PathParam("*") - - if reqPutRepository.ProcessRequestPath(chiCtx, path) { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - arch.UploadPackageFile(ctx) - return - } - - if reqGetRepoArchFile.ProcessRequestPath(chiCtx, path) { - arch.GetPackageOrRepositoryFile(ctx) - return - } - - if reqDeleteRepoNameVerArch.ProcessRequestPath(chiCtx, path) { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - arch.DeletePackageVersion(ctx) - return - } - - ctx.Status(http.StatusNotFound) + r.PathGroup("*", func(g *web.RouterPathGroup) { + g.MatchPath("PUT", "/<repository:*>", reqPackageAccess(perm.AccessModeWrite), arch.UploadPackageFile) + g.MatchPath("HEAD,GET", "/<repository:*>/<architecture>/<filename>", arch.GetPackageOrRepositoryFile) + g.MatchPath("DELETE", "/<repository:*>/<name>/<version>/<architecture>", reqPackageAccess(perm.AccessModeWrite), arch.DeletePackageVersion) }) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/cargo", func() { |