Refactor packages (#34777)

This commit is contained in:
wxiaoguang 2025-06-22 19:22:51 +08:00 committed by GitHub
parent f114c388ff
commit 1748045285
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 149 additions and 247 deletions

View File

@ -6,6 +6,7 @@ package web
import ( import (
"net/http" "net/http"
"regexp" "regexp"
"slices"
"strings" "strings"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
@ -36,11 +37,21 @@ func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request)
g.r.chiRouter.NotFoundHandler().ServeHTTP(resp, req) g.r.chiRouter.NotFoundHandler().ServeHTTP(resp, req)
} }
type RouterPathGroupPattern struct {
re *regexp.Regexp
params []routerPathParam
middlewares []any
}
// MatchPath matches the request method, and uses regexp to match the path. // 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) // 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. // It is only designed to resolve some special cases that 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). // 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) { func (g *RouterPathGroup) MatchPath(methods, pattern string, h ...any) {
g.MatchPattern(methods, g.PatternRegexp(pattern), h...)
}
func (g *RouterPathGroup) MatchPattern(methods string, pattern *RouterPathGroupPattern, h ...any) {
g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...)) g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...))
} }
@ -96,8 +107,8 @@ func isValidMethod(name string) bool {
return false return false
} }
func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher { func newRouterPathMatcher(methods string, patternRegexp *RouterPathGroupPattern, h ...any) *routerPathMatcher {
middlewares, handlerFunc := wrapMiddlewareAndHandler(nil, h) middlewares, handlerFunc := wrapMiddlewareAndHandler(patternRegexp.middlewares, h)
p := &routerPathMatcher{methods: make(container.Set[string]), middlewares: middlewares, handlerFunc: handlerFunc} p := &routerPathMatcher{methods: make(container.Set[string]), middlewares: middlewares, handlerFunc: handlerFunc}
for method := range strings.SplitSeq(methods, ",") { for method := range strings.SplitSeq(methods, ",") {
method = strings.TrimSpace(method) method = strings.TrimSpace(method)
@ -106,19 +117,25 @@ func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher
} }
p.methods.Add(method) p.methods.Add(method)
} }
p.re, p.params = patternRegexp.re, patternRegexp.params
return p
}
func patternRegexp(pattern string, h ...any) *RouterPathGroupPattern {
p := &RouterPathGroupPattern{middlewares: slices.Clone(h)}
re := []byte{'^'} re := []byte{'^'}
lastEnd := 0 lastEnd := 0
for lastEnd < len(pattern) { for lastEnd < len(pattern) {
start := strings.IndexByte(pattern[lastEnd:], '<') start := strings.IndexByte(pattern[lastEnd:], '<')
if start == -1 { if start == -1 {
re = append(re, pattern[lastEnd:]...) re = append(re, regexp.QuoteMeta(pattern[lastEnd:])...)
break break
} }
end := strings.IndexByte(pattern[lastEnd+start:], '>') end := strings.IndexByte(pattern[lastEnd+start:], '>')
if end == -1 { if end == -1 {
panic("invalid pattern: " + pattern) panic("invalid pattern: " + pattern)
} }
re = append(re, pattern[lastEnd:lastEnd+start]...) re = append(re, regexp.QuoteMeta(pattern[lastEnd:lastEnd+start])...)
partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":") partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":")
lastEnd += start + end + 1 lastEnd += start + end + 1
@ -140,7 +157,10 @@ func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher
p.params = append(p.params, param) p.params = append(p.params, param)
} }
re = append(re, '$') re = append(re, '$')
reStr := string(re) p.re = regexp.MustCompile(string(re))
p.re = regexp.MustCompile(reStr)
return p return p
} }
func (g *RouterPathGroup) PatternRegexp(pattern string, h ...any) *RouterPathGroupPattern {
return patternRegexp(pattern, h...)
}

View File

@ -34,7 +34,7 @@ func TestPathProcessor(t *testing.T) {
testProcess := func(pattern, uri string, expectedPathParams map[string]string) { testProcess := func(pattern, uri string, expectedPathParams map[string]string) {
chiCtx := chi.NewRouteContext() chiCtx := chi.NewRouteContext()
chiCtx.RouteMethod = "GET" chiCtx.RouteMethod = "GET"
p := newRouterPathMatcher("GET", pattern, http.NotFound) p := newRouterPathMatcher("GET", patternRegexp(pattern), http.NotFound)
assert.True(t, p.matchPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri) 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) assert.Equal(t, expectedPathParams, chiURLParamsToMap(chiCtx), "use pattern %s to process uri %s", pattern, uri)
} }
@ -58,16 +58,18 @@ func TestRouter(t *testing.T) {
type resultStruct struct { type resultStruct struct {
method string method string
pathParams map[string]string pathParams map[string]string
handlerMark string handlerMarks []string
} }
var res resultStruct
var res resultStruct
h := func(optMark ...string) func(resp http.ResponseWriter, req *http.Request) { h := func(optMark ...string) func(resp http.ResponseWriter, req *http.Request) {
mark := util.OptionalArg(optMark, "") mark := util.OptionalArg(optMark, "")
return func(resp http.ResponseWriter, req *http.Request) { return func(resp http.ResponseWriter, req *http.Request) {
res.method = req.Method res.method = req.Method
res.pathParams = chiURLParamsToMap(chi.RouteContext(req.Context())) res.pathParams = chiURLParamsToMap(chi.RouteContext(req.Context()))
res.handlerMark = mark if mark != "" {
res.handlerMarks = append(res.handlerMarks, mark)
}
} }
} }
@ -77,6 +79,8 @@ func TestRouter(t *testing.T) {
if stop := req.FormValue("stop"); stop != "" && (mark == "" || mark == stop) { if stop := req.FormValue("stop"); stop != "" && (mark == "" || mark == stop) {
h(stop)(resp, req) h(stop)(resp, req)
resp.WriteHeader(http.StatusOK) resp.WriteHeader(http.StatusOK)
} else if mark != "" {
res.handlerMarks = append(res.handlerMarks, mark)
} }
} }
} }
@ -108,7 +112,7 @@ func TestRouter(t *testing.T) {
m.Delete("", h()) m.Delete("", h())
}) })
m.PathGroup("/*", func(g *RouterPathGroup) { m.PathGroup("/*", func(g *RouterPathGroup) {
g.MatchPath("GET", `/<dir:*>/<file:[a-z]{1,2}>`, stopMark("s2"), h("match-path")) g.MatchPattern("GET", g.PatternRegexp(`/<dir:*>/<file:[a-z]{1,2}>`, stopMark("s2")), stopMark("s3"), h("match-path"))
}, stopMark("s1")) }, stopMark("s1"))
}) })
}) })
@ -126,31 +130,31 @@ func TestRouter(t *testing.T) {
} }
t.Run("RootRouter", func(t *testing.T) { 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/other", resultStruct{method: "GET", handlerMarks: []string{"not-found:/"}})
testRoute(t, "GET /the-user/the-repo/pulls", resultStruct{ testRoute(t, "GET /the-user/the-repo/pulls", resultStruct{
method: "GET", method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"}, pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"},
handlerMark: "list-issues-b", handlerMarks: []string{"list-issues-b"},
}) })
testRoute(t, "GET /the-user/the-repo/issues/123", resultStruct{ testRoute(t, "GET /the-user/the-repo/issues/123", resultStruct{
method: "GET", method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
handlerMark: "view-issue", handlerMarks: []string{"view-issue"},
}) })
testRoute(t, "GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{ testRoute(t, "GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{
method: "GET", method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
handlerMark: "hijack", handlerMarks: []string{"hijack"},
}) })
testRoute(t, "POST /the-user/the-repo/issues/123/update", resultStruct{ testRoute(t, "POST /the-user/the-repo/issues/123/update", resultStruct{
method: "POST", method: "POST",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"}, pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"},
handlerMark: "update-issue", handlerMarks: []string{"update-issue"},
}) })
}) })
t.Run("Sub Router", func(t *testing.T) { t.Run("Sub Router", func(t *testing.T) {
testRoute(t, "GET /api/v1/other", resultStruct{method: "GET", handlerMark: "not-found:/api/v1"}) testRoute(t, "GET /api/v1/other", resultStruct{method: "GET", handlerMarks: []string{"not-found:/api/v1"}})
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches", resultStruct{ testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches", resultStruct{
method: "GET", method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"}, pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"},
@ -181,29 +185,35 @@ func TestRouter(t *testing.T) {
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn", resultStruct{ testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn", resultStruct{
method: "GET", method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"},
handlerMark: "match-path", handlerMarks: []string{"s1", "s2", "s3", "match-path"},
}) })
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1%2fd2/fn", resultStruct{ testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1%2fd2/fn", resultStruct{
method: "GET", method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1%2fd2/fn", "dir": "d1%2fd2", "file": "fn"}, pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1%2fd2/fn", "dir": "d1%2fd2", "file": "fn"},
handlerMark: "match-path", handlerMarks: []string{"s1", "s2", "s3", "match-path"},
}) })
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/000", resultStruct{ testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/000", resultStruct{
method: "GET", method: "GET",
pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"}, pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"},
handlerMark: "not-found:/api/v1", handlerMarks: []string{"s1", "not-found:/api/v1"},
}) })
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s1", resultStruct{ testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s1", resultStruct{
method: "GET", method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn"}, pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn"},
handlerMark: "s1", handlerMarks: []string{"s1"},
}) })
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s2", resultStruct{ testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s2", resultStruct{
method: "GET", method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"},
handlerMark: "s2", handlerMarks: []string{"s1", "s2"},
})
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s3", resultStruct{
method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"},
handlerMarks: []string{"s1", "s2", "s3"},
}) })
}) })
} }

View File

@ -5,8 +5,6 @@ package packages
import ( import (
"net/http" "net/http"
"regexp"
"strings"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
@ -282,42 +280,10 @@ func CommonRoutes() *web.Router {
}) })
}) })
}, reqPackageAccess(perm.AccessModeRead)) }, reqPackageAccess(perm.AccessModeRead))
r.Group("/conda", func() { r.PathGroup("/conda/*", func(g *web.RouterPathGroup) {
var ( g.MatchPath("GET", "/<architecture>/<filename>", conda.ListOrGetPackages)
downloadPattern = regexp.MustCompile(`\A(.+/)?(.+)/((?:[^/]+(?:\.tar\.bz2|\.conda))|(?:current_)?repodata\.json(?:\.bz2)?)\z`) g.MatchPath("GET", "/<channel:*>/<architecture>/<filename>", conda.ListOrGetPackages)
uploadPattern = regexp.MustCompile(`\A(.+/)?([^/]+(?:\.tar\.bz2|\.conda))\z`) g.MatchPath("PUT", "/<channel:*>/<filename>", reqPackageAccess(perm.AccessModeWrite), conda.UploadPackageFile)
)
r.Get("/*", func(ctx *context.Context) {
m := downloadPattern.FindStringSubmatch(ctx.PathParam("*"))
if len(m) == 0 {
ctx.Status(http.StatusNotFound)
return
}
ctx.SetPathParam("channel", strings.TrimSuffix(m[1], "/"))
ctx.SetPathParam("architecture", m[2])
ctx.SetPathParam("filename", m[3])
switch m[3] {
case "repodata.json", "repodata.json.bz2", "current_repodata.json", "current_repodata.json.bz2":
conda.EnumeratePackages(ctx)
default:
conda.DownloadPackageFile(ctx)
}
})
r.Put("/*", reqPackageAccess(perm.AccessModeWrite), func(ctx *context.Context) {
m := uploadPattern.FindStringSubmatch(ctx.PathParam("*"))
if len(m) == 0 {
ctx.Status(http.StatusNotFound)
return
}
ctx.SetPathParam("channel", strings.TrimSuffix(m[1], "/"))
ctx.SetPathParam("filename", m[2])
conda.UploadPackageFile(ctx)
})
}, reqPackageAccess(perm.AccessModeRead)) }, reqPackageAccess(perm.AccessModeRead))
r.Group("/cran", func() { r.Group("/cran", func() {
r.Group("/src", func() { r.Group("/src", func() {
@ -358,60 +324,15 @@ func CommonRoutes() *web.Router {
}, reqPackageAccess(perm.AccessModeRead)) }, reqPackageAccess(perm.AccessModeRead))
r.Group("/go", func() { r.Group("/go", func() {
r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), goproxy.UploadPackage) r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), goproxy.UploadPackage)
r.Get("/sumdb/sum.golang.org/supported", func(ctx *context.Context) { r.Get("/sumdb/sum.golang.org/supported", http.NotFound)
ctx.Status(http.StatusNotFound)
})
// Manual mapping of routes because the package name contains slashes which chi does not support
// https://go.dev/ref/mod#goproxy-protocol // https://go.dev/ref/mod#goproxy-protocol
r.Get("/*", func(ctx *context.Context) { r.PathGroup("/*", func(g *web.RouterPathGroup) {
path := ctx.PathParam("*") g.MatchPath("GET", "/<name:*>/@<version:latest>", goproxy.PackageVersionMetadata)
g.MatchPath("GET", "/<name:*>/@v/list", goproxy.EnumeratePackageVersions)
if strings.HasSuffix(path, "/@latest") { g.MatchPath("GET", "/<name:*>/@v/<version>.zip", goproxy.DownloadPackageFile)
ctx.SetPathParam("name", path[:len(path)-len("/@latest")]) g.MatchPath("GET", "/<name:*>/@v/<version>.info", goproxy.PackageVersionMetadata)
ctx.SetPathParam("version", "latest") g.MatchPath("GET", "/<name:*>/@v/<version>.mod", goproxy.PackageVersionGoModContent)
goproxy.PackageVersionMetadata(ctx)
return
}
parts := strings.SplitN(path, "/@v/", 2)
if len(parts) != 2 {
ctx.Status(http.StatusNotFound)
return
}
ctx.SetPathParam("name", parts[0])
// <package/name>/@v/list
if parts[1] == "list" {
goproxy.EnumeratePackageVersions(ctx)
return
}
// <package/name>/@v/<version>.zip
if strings.HasSuffix(parts[1], ".zip") {
ctx.SetPathParam("version", parts[1][:len(parts[1])-len(".zip")])
goproxy.DownloadPackageFile(ctx)
return
}
// <package/name>/@v/<version>.info
if strings.HasSuffix(parts[1], ".info") {
ctx.SetPathParam("version", parts[1][:len(parts[1])-len(".info")])
goproxy.PackageVersionMetadata(ctx)
return
}
// <package/name>/@v/<version>.mod
if strings.HasSuffix(parts[1], ".mod") {
ctx.SetPathParam("version", parts[1][:len(parts[1])-len(".mod")])
goproxy.PackageVersionGoModContent(ctx)
return
}
ctx.Status(http.StatusNotFound)
}) })
}, reqPackageAccess(perm.AccessModeRead)) }, reqPackageAccess(perm.AccessModeRead))
r.Group("/generic", func() { r.Group("/generic", func() {
@ -532,82 +453,24 @@ func CommonRoutes() *web.Router {
}) })
}) })
}, reqPackageAccess(perm.AccessModeRead)) }, reqPackageAccess(perm.AccessModeRead))
r.Group("/pypi", func() { r.Group("/pypi", func() {
r.Post("/", reqPackageAccess(perm.AccessModeWrite), pypi.UploadPackageFile) r.Post("/", reqPackageAccess(perm.AccessModeWrite), pypi.UploadPackageFile)
r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile) r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile)
r.Get("/simple/{id}", pypi.PackageMetadata) r.Get("/simple/{id}", pypi.PackageMetadata)
}, reqPackageAccess(perm.AccessModeRead)) }, reqPackageAccess(perm.AccessModeRead))
r.Group("/rpm", func() {
r.Group("/repository.key", func() {
r.Head("", rpm.GetRepositoryKey)
r.Get("", rpm.GetRepositoryKey)
})
var ( r.Methods("HEAD,GET", "/rpm.repo", reqPackageAccess(perm.AccessModeRead), rpm.GetRepositoryConfig)
repoPattern = regexp.MustCompile(`\A(.*?)\.repo\z`) r.PathGroup("/rpm/*", func(g *web.RouterPathGroup) {
uploadPattern = regexp.MustCompile(`\A(.*?)/upload\z`) g.MatchPath("HEAD,GET", "/repository.key", rpm.GetRepositoryKey)
filePattern = regexp.MustCompile(`\A(.*?)/package/([^/]+)/([^/]+)/([^/]+)(?:/([^/]+\.rpm)|)\z`) g.MatchPath("HEAD,GET", "/<group:*>.repo", rpm.GetRepositoryConfig)
repoFilePattern = regexp.MustCompile(`\A(.*?)/repodata/([^/]+)\z`) g.MatchPath("HEAD", "/<group:*>/repodata/<filename>", rpm.CheckRepositoryFileExistence)
) g.MatchPath("GET", "/<group:*>/repodata/<filename>", rpm.GetRepositoryFile)
g.MatchPath("PUT", "/<group:*>/upload", reqPackageAccess(perm.AccessModeWrite), rpm.UploadPackageFile)
r.Methods("HEAD,GET,PUT,DELETE", "*", func(ctx *context.Context) { g.MatchPath("HEAD,GET", "/<group:*>/package/<name>/<version>/<architecture>", rpm.DownloadPackageFile)
path := ctx.PathParam("*") g.MatchPath("DELETE", "/<group:*>/package/<name>/<version>/<architecture>", reqPackageAccess(perm.AccessModeWrite), rpm.DeletePackageFile)
isHead := ctx.Req.Method == http.MethodHead
isGetHead := ctx.Req.Method == http.MethodHead || ctx.Req.Method == http.MethodGet
isPut := ctx.Req.Method == http.MethodPut
isDelete := ctx.Req.Method == http.MethodDelete
m := repoPattern.FindStringSubmatch(path)
if len(m) == 2 && isGetHead {
ctx.SetPathParam("group", strings.Trim(m[1], "/"))
rpm.GetRepositoryConfig(ctx)
return
}
m = repoFilePattern.FindStringSubmatch(path)
if len(m) == 3 && isGetHead {
ctx.SetPathParam("group", strings.Trim(m[1], "/"))
ctx.SetPathParam("filename", m[2])
if isHead {
rpm.CheckRepositoryFileExistence(ctx)
} else {
rpm.GetRepositoryFile(ctx)
}
return
}
m = uploadPattern.FindStringSubmatch(path)
if len(m) == 2 && isPut {
reqPackageAccess(perm.AccessModeWrite)(ctx)
if ctx.Written() {
return
}
ctx.SetPathParam("group", strings.Trim(m[1], "/"))
rpm.UploadPackageFile(ctx)
return
}
m = filePattern.FindStringSubmatch(path)
if len(m) == 6 && (isGetHead || isDelete) {
ctx.SetPathParam("group", strings.Trim(m[1], "/"))
ctx.SetPathParam("name", m[2])
ctx.SetPathParam("version", m[3])
ctx.SetPathParam("architecture", m[4])
if isGetHead {
rpm.DownloadPackageFile(ctx)
} else {
reqPackageAccess(perm.AccessModeWrite)(ctx)
if ctx.Written() {
return
}
rpm.DeletePackageFile(ctx)
}
return
}
ctx.Status(http.StatusNotFound)
})
}, reqPackageAccess(perm.AccessModeRead)) }, reqPackageAccess(perm.AccessModeRead))
r.Group("/rubygems", func() { r.Group("/rubygems", func() {
r.Get("/specs.4.8.gz", rubygems.EnumeratePackages) r.Get("/specs.4.8.gz", rubygems.EnumeratePackages)
r.Get("/latest_specs.4.8.gz", rubygems.EnumeratePackagesLatest) r.Get("/latest_specs.4.8.gz", rubygems.EnumeratePackagesLatest)
@ -621,6 +484,7 @@ func CommonRoutes() *web.Router {
r.Delete("/yank", rubygems.DeletePackage) r.Delete("/yank", rubygems.DeletePackage)
}, reqPackageAccess(perm.AccessModeWrite)) }, reqPackageAccess(perm.AccessModeWrite))
}, reqPackageAccess(perm.AccessModeRead)) }, reqPackageAccess(perm.AccessModeRead))
r.Group("/swift", func() { r.Group("/swift", func() {
r.Group("", func() { // Needs to be unauthenticated. r.Group("", func() { // Needs to be unauthenticated.
r.Post("", swift.CheckAuthenticate) r.Post("", swift.CheckAuthenticate)
@ -632,31 +496,12 @@ func CommonRoutes() *web.Router {
r.Get("", swift.EnumeratePackageVersions) r.Get("", swift.EnumeratePackageVersions)
r.Get(".json", swift.EnumeratePackageVersions) r.Get(".json", swift.EnumeratePackageVersions)
}, swift.CheckAcceptMediaType(swift.AcceptJSON)) }, swift.CheckAcceptMediaType(swift.AcceptJSON))
r.Group("/{version}", func() { r.PathGroup("/*", func(g *web.RouterPathGroup) {
r.Get("/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest) g.MatchPath("GET", "/<version>.json", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.PackageVersionMetadata)
r.Put("", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile) g.MatchPath("GET", "/<version>.zip", swift.CheckAcceptMediaType(swift.AcceptZip), swift.DownloadPackageFile)
r.Get("", func(ctx *context.Context) { g.MatchPath("GET", "/<version>/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest)
// Can't use normal routes here: https://github.com/go-chi/chi/issues/781 g.MatchPath("GET", "/<version>", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.PackageVersionMetadata)
g.MatchPath("PUT", "/<version>", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile)
version := ctx.PathParam("version")
if strings.HasSuffix(version, ".zip") {
swift.CheckAcceptMediaType(swift.AcceptZip)(ctx)
if ctx.Written() {
return
}
ctx.SetPathParam("version", version[:len(version)-4])
swift.DownloadPackageFile(ctx)
} else {
swift.CheckAcceptMediaType(swift.AcceptJSON)(ctx)
if ctx.Written() {
return
}
if strings.HasSuffix(version, ".json") {
ctx.SetPathParam("version", version[:len(version)-5])
}
swift.PackageVersionMetadata(ctx)
}
})
}) })
}) })
r.Get("/identifiers", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.LookupPackageIdentifiers) r.Get("/identifiers", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.LookupPackageIdentifiers)
@ -705,18 +550,13 @@ func ContainerRoutes() *web.Router {
r.PathGroup("/*", func(g *web.RouterPathGroup) { r.PathGroup("/*", func(g *web.RouterPathGroup) {
g.MatchPath("POST", "/<image:*>/blobs/uploads", reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, container.PostBlobsUploads) g.MatchPath("POST", "/<image:*>/blobs/uploads", reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, container.PostBlobsUploads)
g.MatchPath("GET", "/<image:*>/tags/list", container.VerifyImageName, container.GetTagsList) g.MatchPath("GET", "/<image:*>/tags/list", container.VerifyImageName, container.GetTagsList)
g.MatchPath("GET,PATCH,PUT,DELETE", `/<image:*>/blobs/uploads/<uuid:[-.=\w]+>`, reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, func(ctx *context.Context) {
switch ctx.Req.Method { patternBlobsUploadsUUID := g.PatternRegexp(`/<image:*>/blobs/uploads/<uuid:[-.=\w]+>`, reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName)
case http.MethodGet: g.MatchPattern("GET", patternBlobsUploadsUUID, container.GetBlobsUpload)
container.GetBlobsUpload(ctx) g.MatchPattern("PATCH", patternBlobsUploadsUUID, container.PatchBlobsUpload)
case http.MethodPatch: g.MatchPattern("PUT", patternBlobsUploadsUUID, container.PutBlobsUpload)
container.PatchBlobsUpload(ctx) g.MatchPattern("DELETE", patternBlobsUploadsUUID, container.DeleteBlobsUpload)
case http.MethodPut:
container.PutBlobsUpload(ctx)
default: /* DELETE */
container.DeleteBlobsUpload(ctx)
}
})
g.MatchPath("HEAD", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.HeadBlob) g.MatchPath("HEAD", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.HeadBlob)
g.MatchPath("GET", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.GetBlob) g.MatchPath("GET", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.GetBlob)
g.MatchPath("DELETE", `/<image:*>/blobs/<digest>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.DeleteBlob) g.MatchPath("DELETE", `/<image:*>/blobs/<digest>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.DeleteBlob)

View File

@ -36,6 +36,24 @@ func apiError(ctx *context.Context, status int, obj any) {
}) })
} }
func isCondaPackageFileName(filename string) bool {
return strings.HasSuffix(filename, ".tar.bz2") || strings.HasSuffix(filename, ".conda")
}
func ListOrGetPackages(ctx *context.Context) {
filename := ctx.PathParam("filename")
switch filename {
case "repodata.json", "repodata.json.bz2", "current_repodata.json", "current_repodata.json.bz2":
EnumeratePackages(ctx)
return
}
if isCondaPackageFileName(filename) {
DownloadPackageFile(ctx)
return
}
ctx.NotFound(nil)
}
func EnumeratePackages(ctx *context.Context) { func EnumeratePackages(ctx *context.Context) {
type Info struct { type Info struct {
Subdir string `json:"subdir"` Subdir string `json:"subdir"`
@ -174,6 +192,12 @@ func EnumeratePackages(ctx *context.Context) {
} }
func UploadPackageFile(ctx *context.Context) { func UploadPackageFile(ctx *context.Context) {
filename := ctx.PathParam("filename")
if !isCondaPackageFileName(filename) {
apiError(ctx, http.StatusBadRequest, nil)
return
}
upload, needToClose, err := ctx.UploadStream() upload, needToClose, err := ctx.UploadStream()
if err != nil { if err != nil {
apiError(ctx, http.StatusInternalServerError, err) apiError(ctx, http.StatusInternalServerError, err)
@ -191,7 +215,7 @@ func UploadPackageFile(ctx *context.Context) {
defer buf.Close() defer buf.Close()
var pck *conda_module.Package var pck *conda_module.Package
if strings.HasSuffix(strings.ToLower(ctx.PathParam("filename")), ".tar.bz2") { if strings.HasSuffix(filename, ".tar.bz2") {
pck, err = conda_module.ParsePackageBZ2(buf) pck, err = conda_module.ParsePackageBZ2(buf)
} else { } else {
pck, err = conda_module.ParsePackageConda(buf, buf.Size()) pck, err = conda_module.ParsePackageConda(buf, buf.Size())

View File

@ -90,14 +90,14 @@ func mountBlob(ctx context.Context, pi *packages_service.PackageInfo, pb *packag
}) })
} }
func containerPkgName(piOwnerID int64, piName string) string { func containerGlobalLockKey(piOwnerID int64, piName, usage string) string {
return fmt.Sprintf("pkg_%d_container_%s", piOwnerID, strings.ToLower(piName)) return fmt.Sprintf("pkg_%d_container_%s_%s", piOwnerID, strings.ToLower(piName), usage)
} }
func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageInfo) (*packages_model.PackageVersion, error) { func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageInfo) (*packages_model.PackageVersion, error) {
var uploadVersion *packages_model.PackageVersion var uploadVersion *packages_model.PackageVersion
releaser, err := globallock.Lock(ctx, containerPkgName(pi.Owner.ID, pi.Name)) releaser, err := globallock.Lock(ctx, containerGlobalLockKey(pi.Owner.ID, pi.Name, "package"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -178,7 +178,7 @@ func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, p
} }
func deleteBlob(ctx context.Context, ownerID int64, image string, digest digest.Digest) error { func deleteBlob(ctx context.Context, ownerID int64, image string, digest digest.Digest) error {
releaser, err := globallock.Lock(ctx, containerPkgName(ownerID, image)) releaser, err := globallock.Lock(ctx, containerGlobalLockKey(ownerID, image, "blob"))
if err != nil { if err != nil {
return err return err
} }

View File

@ -32,7 +32,7 @@ import (
packages_service "code.gitea.io/gitea/services/packages" packages_service "code.gitea.io/gitea/services/packages"
container_service "code.gitea.io/gitea/services/packages/container" container_service "code.gitea.io/gitea/services/packages/container"
digest "github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
) )
// maximum size of a container manifest // maximum size of a container manifest

View File

@ -16,6 +16,7 @@ import (
packages_model "code.gitea.io/gitea/models/packages" packages_model "code.gitea.io/gitea/models/packages"
container_model "code.gitea.io/gitea/models/packages/container" container_model "code.gitea.io/gitea/models/packages/container"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/globallock"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages" packages_module "code.gitea.io/gitea/modules/packages"
@ -61,6 +62,13 @@ func processManifest(ctx context.Context, mci *manifestCreationInfo, buf *packag
} }
} }
// .../container/manifest.go:453:createManifestBlob() [E] Error inserting package blob: Error 1062 (23000): Duplicate entry '..........' for key 'package_blob.UQE_package_blob_md5'
releaser, err := globallock.Lock(ctx, containerGlobalLockKey(mci.Owner.ID, mci.Image, "manifest"))
if err != nil {
return "", err
}
defer releaser()
if container_module.IsMediaTypeImageManifest(mci.MediaType) { if container_module.IsMediaTypeImageManifest(mci.MediaType) {
return processOciImageManifest(ctx, mci, buf) return processOciImageManifest(ctx, mci, buf)
} else if container_module.IsMediaTypeImageIndex(mci.MediaType) { } else if container_module.IsMediaTypeImageIndex(mci.MediaType) {

View File

@ -13,7 +13,7 @@ import (
container_module "code.gitea.io/gitea/modules/packages/container" container_module "code.gitea.io/gitea/modules/packages/container"
packages_service "code.gitea.io/gitea/services/packages" packages_service "code.gitea.io/gitea/services/packages"
digest "github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
) )
// Cleanup removes expired container data // Cleanup removes expired container data