* Added properties for packages. * Fixed authenticate header format. * Added _catalog endpoint. * Check owner visibility. * Extracted condition. * Added test for _catalog. Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: Lauris BH <lauris@nix.lv> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>tags/v1.18.0-rc0
@@ -27,6 +27,7 @@ import ( | |||
func TestPackageContainer(t *testing.T) { | |||
defer prepareTestEnv(t)() | |||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) | |||
has := func(l packages_model.PackagePropertyList, name string) bool { | |||
@@ -37,6 +38,15 @@ func TestPackageContainer(t *testing.T) { | |||
} | |||
return false | |||
} | |||
getAllByName := func(l packages_model.PackagePropertyList, name string) []string { | |||
values := make([]string, 0, len(l)) | |||
for _, pp := range l { | |||
if pp.Name == name { | |||
values = append(values, pp.Value) | |||
} | |||
} | |||
return values | |||
} | |||
images := []string{"test", "te/st"} | |||
tags := []string{"latest", "main"} | |||
@@ -67,7 +77,7 @@ func TestPackageContainer(t *testing.T) { | |||
Token string `json:"token"` | |||
} | |||
authenticate := []string{`Bearer realm="` + setting.AppURL + `v2/token"`} | |||
authenticate := []string{`Bearer realm="` + setting.AppURL + `v2/token",service="container_registry",scope="*"`} | |||
t.Run("Anonymous", func(t *testing.T) { | |||
defer PrintCurrentTest(t)() | |||
@@ -237,7 +247,8 @@ func TestPackageContainer(t *testing.T) { | |||
assert.Nil(t, pd.SemVer) | |||
assert.Equal(t, image, pd.Package.Name) | |||
assert.Equal(t, tag, pd.Version.Version) | |||
assert.True(t, has(pd.Properties, container_module.PropertyManifestTagged)) | |||
assert.ElementsMatch(t, []string{strings.ToLower(user.LowerName + "/" + image)}, getAllByName(pd.PackageProperties, container_module.PropertyRepository)) | |||
assert.True(t, has(pd.VersionProperties, container_module.PropertyManifestTagged)) | |||
assert.IsType(t, &container_module.Metadata{}, pd.Metadata) | |||
metadata := pd.Metadata.(*container_module.Metadata) | |||
@@ -331,7 +342,8 @@ func TestPackageContainer(t *testing.T) { | |||
assert.Nil(t, pd.SemVer) | |||
assert.Equal(t, image, pd.Package.Name) | |||
assert.Equal(t, untaggedManifestDigest, pd.Version.Version) | |||
assert.False(t, has(pd.Properties, container_module.PropertyManifestTagged)) | |||
assert.ElementsMatch(t, []string{strings.ToLower(user.LowerName + "/" + image)}, getAllByName(pd.PackageProperties, container_module.PropertyRepository)) | |||
assert.False(t, has(pd.VersionProperties, container_module.PropertyManifestTagged)) | |||
assert.IsType(t, &container_module.Metadata{}, pd.Metadata) | |||
@@ -363,18 +375,10 @@ func TestPackageContainer(t *testing.T) { | |||
assert.Nil(t, pd.SemVer) | |||
assert.Equal(t, image, pd.Package.Name) | |||
assert.Equal(t, multiTag, pd.Version.Version) | |||
assert.True(t, has(pd.Properties, container_module.PropertyManifestTagged)) | |||
assert.ElementsMatch(t, []string{strings.ToLower(user.LowerName + "/" + image)}, getAllByName(pd.PackageProperties, container_module.PropertyRepository)) | |||
assert.True(t, has(pd.VersionProperties, container_module.PropertyManifestTagged)) | |||
getAllByName := func(l packages_model.PackagePropertyList, name string) []string { | |||
values := make([]string, 0, len(l)) | |||
for _, pp := range l { | |||
if pp.Name == name { | |||
values = append(values, pp.Value) | |||
} | |||
} | |||
return values | |||
} | |||
assert.ElementsMatch(t, []string{manifestDigest, untaggedManifestDigest}, getAllByName(pd.Properties, container_module.PropertyManifestReference)) | |||
assert.ElementsMatch(t, []string{manifestDigest, untaggedManifestDigest}, getAllByName(pd.VersionProperties, container_module.PropertyManifestReference)) | |||
assert.IsType(t, &container_module.Metadata{}, pd.Metadata) | |||
metadata := pd.Metadata.(*container_module.Metadata) | |||
@@ -536,4 +540,56 @@ func TestPackageContainer(t *testing.T) { | |||
}) | |||
}) | |||
} | |||
t.Run("OwnerNameChange", func(t *testing.T) { | |||
defer PrintCurrentTest(t)() | |||
checkCatalog := func(owner string) func(t *testing.T) { | |||
return func(t *testing.T) { | |||
defer PrintCurrentTest(t)() | |||
req := NewRequest(t, "GET", fmt.Sprintf("%sv2/_catalog", setting.AppURL)) | |||
addTokenAuthHeader(req, userToken) | |||
resp := MakeRequest(t, req, http.StatusOK) | |||
type RepositoryList struct { | |||
Repositories []string `json:"repositories"` | |||
} | |||
repoList := &RepositoryList{} | |||
DecodeJSON(t, resp, &repoList) | |||
assert.Len(t, repoList.Repositories, len(images)) | |||
names := make([]string, 0, len(images)) | |||
for _, image := range images { | |||
names = append(names, strings.ToLower(owner+"/"+image)) | |||
} | |||
assert.ElementsMatch(t, names, repoList.Repositories) | |||
} | |||
} | |||
t.Run(fmt.Sprintf("Catalog[%s]", user.LowerName), checkCatalog(user.LowerName)) | |||
session := loginUser(t, user.Name) | |||
newOwnerName := "newUsername" | |||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{ | |||
"_csrf": GetCSRF(t, session, "/user/settings"), | |||
"name": newOwnerName, | |||
"email": "user2@example.com", | |||
"language": "en-US", | |||
}) | |||
session.MakeRequest(t, req, http.StatusSeeOther) | |||
t.Run(fmt.Sprintf("Catalog[%s]", newOwnerName), checkCatalog(newOwnerName)) | |||
req = NewRequestWithValues(t, "POST", "/user/settings", map[string]string{ | |||
"_csrf": GetCSRF(t, session, "/user/settings"), | |||
"name": user.Name, | |||
"email": "user2@example.com", | |||
"language": "en-US", | |||
}) | |||
session.MakeRequest(t, req, http.StatusSeeOther) | |||
}) | |||
} |
@@ -85,9 +85,9 @@ func TestPackageNpm(t *testing.T) { | |||
assert.IsType(t, &npm.Metadata{}, pd.Metadata) | |||
assert.Equal(t, packageName, pd.Package.Name) | |||
assert.Equal(t, packageVersion, pd.Version.Version) | |||
assert.Len(t, pd.Properties, 1) | |||
assert.Equal(t, npm.TagProperty, pd.Properties[0].Name) | |||
assert.Equal(t, packageTag, pd.Properties[0].Value) | |||
assert.Len(t, pd.VersionProperties, 1) | |||
assert.Equal(t, npm.TagProperty, pd.VersionProperties[0].Name) | |||
assert.Equal(t, packageTag, pd.VersionProperties[0].Value) | |||
pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) | |||
assert.NoError(t, err) |
@@ -398,6 +398,8 @@ var migrations = []Migration{ | |||
NewMigration("Improve Action table indices v2", improveActionTableIndices), | |||
// v219 -> v220 | |||
NewMigration("Add sync_on_commit column to push_mirror table", addSyncOnCommitColForPushMirror), | |||
// v220 -> v221 | |||
NewMigration("Add container repository property", addContainerRepositoryProperty), | |||
} | |||
// GetCurrentDBVersion returns the current db version |
@@ -0,0 +1,29 @@ | |||
// Copyright 2022 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 migrations | |||
import ( | |||
packages_model "code.gitea.io/gitea/models/packages" | |||
container_module "code.gitea.io/gitea/modules/packages/container" | |||
"xorm.io/xorm" | |||
"xorm.io/xorm/schemas" | |||
) | |||
func addContainerRepositoryProperty(x *xorm.Engine) error { | |||
switch x.Dialect().URI().DBType { | |||
case schemas.SQLITE: | |||
_, err := x.Exec("INSERT INTO package_property (ref_type, ref_id, name, value) SELECT ?, p.id, ?, u.lower_name || '/' || p.lower_name FROM package p JOIN `user` u ON p.owner_id = u.id WHERE p.type = ?", packages_model.PropertyTypePackage, container_module.PropertyRepository, packages_model.TypeContainer) | |||
if err != nil { | |||
return err | |||
} | |||
default: | |||
_, err := x.Exec("INSERT INTO package_property (ref_type, ref_id, name, value) SELECT ?, p.id, ?, CONCAT(u.lower_name, '/', p.lower_name) FROM package p JOIN `user` u ON p.owner_id = u.id WHERE p.type = ?", packages_model.PropertyTypePackage, container_module.PropertyRepository, packages_model.TypeContainer) | |||
if err != nil { | |||
return err | |||
} | |||
} | |||
return nil | |||
} |
@@ -12,6 +12,7 @@ import ( | |||
"code.gitea.io/gitea/models/db" | |||
"code.gitea.io/gitea/models/packages" | |||
user_model "code.gitea.io/gitea/models/user" | |||
container_module "code.gitea.io/gitea/modules/packages/container" | |||
"xorm.io/builder" | |||
@@ -210,6 +211,7 @@ func SearchImageTags(ctx context.Context, opts *ImageTagsSearchOptions) ([]*pack | |||
return pvs, count, err | |||
} | |||
// SearchExpiredUploadedBlobs gets all uploaded blobs which are older than specified | |||
func SearchExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) ([]*packages.PackageFile, error) { | |||
var cond builder.Cond = builder.Eq{ | |||
"package_version.is_internal": true, | |||
@@ -225,3 +227,37 @@ func SearchExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) ([ | |||
Where(cond). | |||
Find(&pfs) | |||
} | |||
// GetRepositories gets a sorted list of all repositories | |||
func GetRepositories(ctx context.Context, actor *user_model.User, n int, last string) ([]string, error) { | |||
var cond builder.Cond = builder.Eq{ | |||
"package.type": packages.TypeContainer, | |||
"package_property.ref_type": packages.PropertyTypePackage, | |||
"package_property.name": container_module.PropertyRepository, | |||
} | |||
cond = cond.And(builder.Exists( | |||
builder. | |||
Select("package_version.id"). | |||
Where(builder.Eq{"package_version.is_internal": false}.And(builder.Expr("package.id = package_version.package_id"))). | |||
From("package_version"), | |||
)) | |||
if last != "" { | |||
cond = cond.And(builder.Gt{"package_property.value": strings.ToLower(last)}) | |||
} | |||
cond = cond.And(user_model.BuildCanSeeUserCondition(actor)) | |||
sess := db.GetEngine(ctx). | |||
Table("package"). | |||
Select("package_property.value"). | |||
Join("INNER", "user", "`user`.id = package.owner_id"). | |||
Join("INNER", "package_property", "package_property.ref_id = package.id"). | |||
Where(cond). | |||
Asc("package_property.value"). | |||
Limit(n) | |||
repositories := make([]string, 0, n) | |||
return repositories, sess.Find(&repositories) | |||
} |
@@ -40,15 +40,16 @@ func (l PackagePropertyList) GetByName(name string) string { | |||
// PackageDescriptor describes a package | |||
type PackageDescriptor struct { | |||
Package *Package | |||
Owner *user_model.User | |||
Repository *repo_model.Repository | |||
Version *PackageVersion | |||
SemVer *version.Version | |||
Creator *user_model.User | |||
Properties PackagePropertyList | |||
Metadata interface{} | |||
Files []*PackageFileDescriptor | |||
Package *Package | |||
Owner *user_model.User | |||
Repository *repo_model.Repository | |||
Version *PackageVersion | |||
SemVer *version.Version | |||
Creator *user_model.User | |||
PackageProperties PackagePropertyList | |||
VersionProperties PackagePropertyList | |||
Metadata interface{} | |||
Files []*PackageFileDescriptor | |||
} | |||
// PackageFileDescriptor describes a package file | |||
@@ -102,6 +103,10 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc | |||
return nil, err | |||
} | |||
} | |||
pps, err := GetProperties(ctx, PropertyTypePackage, p.ID) | |||
if err != nil { | |||
return nil, err | |||
} | |||
pvps, err := GetProperties(ctx, PropertyTypeVersion, pv.ID) | |||
if err != nil { | |||
return nil, err | |||
@@ -152,15 +157,16 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc | |||
} | |||
return &PackageDescriptor{ | |||
Package: p, | |||
Owner: o, | |||
Repository: repository, | |||
Version: pv, | |||
SemVer: semVer, | |||
Creator: creator, | |||
Properties: PackagePropertyList(pvps), | |||
Metadata: metadata, | |||
Files: pfds, | |||
Package: p, | |||
Owner: o, | |||
Repository: repository, | |||
Version: pv, | |||
SemVer: semVer, | |||
Creator: creator, | |||
PackageProperties: PackagePropertyList(pps), | |||
VersionProperties: PackagePropertyList(pvps), | |||
Metadata: metadata, | |||
Files: pfds, | |||
}, nil | |||
} | |||
@@ -131,6 +131,12 @@ func TryInsertPackage(ctx context.Context, p *Package) (*Package, error) { | |||
return p, nil | |||
} | |||
// DeletePackageByID deletes a package by id | |||
func DeletePackageByID(ctx context.Context, packageID int64) error { | |||
_, err := db.GetEngine(ctx).ID(packageID).Delete(&Package{}) | |||
return err | |||
} | |||
// SetRepositoryLink sets the linked repository | |||
func SetRepositoryLink(ctx context.Context, packageID, repoID int64) error { | |||
_, err := db.GetEngine(ctx).ID(packageID).Cols("repo_id").Update(&Package{RepoID: repoID}) | |||
@@ -192,21 +198,20 @@ func GetPackagesByType(ctx context.Context, ownerID int64, packageType Type) ([] | |||
Find(&ps) | |||
} | |||
// DeletePackagesIfUnreferenced deletes a package if there are no associated versions | |||
func DeletePackagesIfUnreferenced(ctx context.Context) error { | |||
// FindUnreferencedPackages gets all packages without associated versions | |||
func FindUnreferencedPackages(ctx context.Context) ([]*Package, error) { | |||
in := builder. | |||
Select("package.id"). | |||
From("package"). | |||
LeftJoin("package_version", "package_version.package_id = package.id"). | |||
Where(builder.Expr("package_version.id IS NULL")) | |||
_, err := db.GetEngine(ctx). | |||
ps := make([]*Package, 0, 10) | |||
return ps, db.GetEngine(ctx). | |||
// double select workaround for MySQL | |||
// https://stackoverflow.com/questions/4471277/mysql-delete-from-with-subquery-as-condition | |||
Where(builder.In("package.id", builder.Select("id").From(in, "temp"))). | |||
Delete(&Package{}) | |||
return err | |||
Find(&ps) | |||
} | |||
// HasOwnerPackages tests if a user/org has packages |
@@ -21,9 +21,11 @@ const ( | |||
PropertyTypeVersion PropertyType = iota // 0 | |||
// PropertyTypeFile means the reference is a package file | |||
PropertyTypeFile // 1 | |||
// PropertyTypePackage means the reference is a package | |||
PropertyTypePackage // 2 | |||
) | |||
// PackageProperty represents a property of a package version or file | |||
// PackageProperty represents a property of a package, version or file | |||
type PackageProperty struct { | |||
ID int64 `xorm:"pk autoincr"` | |||
RefType PropertyType `xorm:"INDEX NOT NULL"` | |||
@@ -68,3 +70,9 @@ func DeletePropertyByID(ctx context.Context, propertyID int64) error { | |||
_, err := db.GetEngine(ctx).ID(propertyID).Delete(&PackageProperty{}) | |||
return err | |||
} | |||
// DeletePropertyByName deletes properties by name | |||
func DeletePropertyByName(ctx context.Context, refType PropertyType, refID int64, name string) error { | |||
_, err := db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).Delete(&PackageProperty{}) | |||
return err | |||
} |
@@ -58,24 +58,7 @@ func (opts *SearchUserOptions) toSearchQueryBase() *xorm.Session { | |||
cond = cond.And(builder.In("visibility", opts.Visible)) | |||
} | |||
if opts.Actor != nil { | |||
// If Admin - they see all users! | |||
if !opts.Actor.IsAdmin { | |||
// Users can see an organization they are a member of | |||
accessCond := builder.In("id", builder.Select("org_id").From("org_user").Where(builder.Eq{"uid": opts.Actor.ID})) | |||
if !opts.Actor.IsRestricted { | |||
// Not-Restricted users can see public and limited users/organizations | |||
accessCond = accessCond.Or(builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited)) | |||
} | |||
// Don't forget about self | |||
accessCond = accessCond.Or(builder.Eq{"id": opts.Actor.ID}) | |||
cond = cond.And(accessCond) | |||
} | |||
} else { | |||
// Force visibility for privacy | |||
// Not logged in - only public users | |||
cond = cond.And(builder.In("visibility", structs.VisibleTypePublic)) | |||
} | |||
cond = cond.And(BuildCanSeeUserCondition(opts.Actor)) | |||
if opts.UID > 0 { | |||
cond = cond.And(builder.Eq{"id": opts.UID}) | |||
@@ -163,3 +146,26 @@ func IterateUser(f func(user *User) error) error { | |||
} | |||
} | |||
} | |||
// BuildCanSeeUserCondition creates a condition which can be used to restrict results to users/orgs the actor can see | |||
func BuildCanSeeUserCondition(actor *User) builder.Cond { | |||
if actor != nil { | |||
// If Admin - they see all users! | |||
if !actor.IsAdmin { | |||
// Users can see an organization they are a member of | |||
cond := builder.In("`user`.id", builder.Select("org_id").From("org_user").Where(builder.Eq{"uid": actor.ID})) | |||
if !actor.IsRestricted { | |||
// Not-Restricted users can see public and limited users/organizations | |||
cond = cond.Or(builder.In("`user`.visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited)) | |||
} | |||
// Don't forget about self | |||
return cond.Or(builder.Eq{"`user`.id": actor.ID}) | |||
} | |||
return nil | |||
} | |||
// Force visibility for privacy | |||
// Not logged in - only public users | |||
return builder.In("`user`.visibility", structs.VisibleTypePublic) | |||
} |
@@ -16,6 +16,7 @@ import ( | |||
) | |||
const ( | |||
PropertyRepository = "container.repository" | |||
PropertyDigest = "container.digest" | |||
PropertyMediaType = "container.mediatype" | |||
PropertyManifestTagged = "container.manifest.tagged" |
@@ -257,6 +257,7 @@ func ContainerRoutes() *web.Route { | |||
r.Get("", container.ReqContainerAccess, container.DetermineSupport) | |||
r.Get("/token", container.Authenticate) | |||
r.Get("/_catalog", container.ReqContainerAccess, container.GetRepositoryList) | |||
r.Group("/{username}", func() { | |||
r.Group("/{image}", func() { | |||
r.Group("/blobs/uploads", func() { |
@@ -88,7 +88,7 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac | |||
for _, pd := range pds { | |||
packageType := "" | |||
for _, pvp := range pd.Properties { | |||
for _, pvp := range pd.VersionProperties { | |||
if pvp.Name == composer_module.TypeProperty { | |||
packageType = pvp.Value | |||
break |
@@ -227,7 +227,7 @@ func UploadPackage(ctx *context.Context) { | |||
SemverCompatible: true, | |||
Creator: ctx.Doer, | |||
Metadata: cp.Metadata, | |||
Properties: map[string]string{ | |||
VersionProperties: map[string]string{ | |||
composer_module.TypeProperty: cp.Type, | |||
}, | |||
}, |
@@ -29,6 +29,7 @@ func saveAsPackageBlob(hsr packages_module.HashedSizeReader, pi *packages_servic | |||
contentStore := packages_module.NewContentStore() | |||
err := db.WithTx(func(ctx context.Context) error { | |||
created := true | |||
p := &packages_model.Package{ | |||
OwnerID: pi.Owner.ID, | |||
Type: packages_model.TypeContainer, | |||
@@ -37,12 +38,21 @@ func saveAsPackageBlob(hsr packages_module.HashedSizeReader, pi *packages_servic | |||
} | |||
var err error | |||
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil { | |||
if err != packages_model.ErrDuplicatePackage { | |||
if err == packages_model.ErrDuplicatePackage { | |||
created = false | |||
} else { | |||
log.Error("Error inserting package: %v", err) | |||
return err | |||
} | |||
} | |||
if created { | |||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository, strings.ToLower(pi.Owner.LowerName+"/"+pi.Name)); err != nil { | |||
log.Error("Error setting package property: %v", err) | |||
return err | |||
} | |||
} | |||
pv := &packages_model.PackageVersion{ | |||
PackageID: p.ID, | |||
CreatorID: pi.Owner.ID, |
@@ -112,7 +112,7 @@ func apiErrorDefined(ctx *context.Context, err *namedError) { | |||
// ReqContainerAccess is a middleware which checks the current user valid (real user or ghost for anonymous access) | |||
func ReqContainerAccess(ctx *context.Context) { | |||
if ctx.Doer == nil { | |||
ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token"`) | |||
ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token",service="container_registry",scope="*"`) | |||
apiErrorDefined(ctx, errUnauthorized) | |||
} | |||
} | |||
@@ -151,6 +151,39 @@ func Authenticate(ctx *context.Context) { | |||
}) | |||
} | |||
// https://docs.docker.com/registry/spec/api/#listing-repositories | |||
func GetRepositoryList(ctx *context.Context) { | |||
n := ctx.FormInt("n") | |||
if n <= 0 || n > 100 { | |||
n = 100 | |||
} | |||
last := ctx.FormTrim("last") | |||
repositories, err := container_model.GetRepositories(ctx, ctx.Doer, n, last) | |||
if err != nil { | |||
apiError(ctx, http.StatusInternalServerError, err) | |||
return | |||
} | |||
type RepositoryList struct { | |||
Repositories []string `json:"repositories"` | |||
} | |||
if len(repositories) == n { | |||
v := url.Values{} | |||
if n > 0 { | |||
v.Add("n", strconv.Itoa(n)) | |||
} | |||
v.Add("last", repositories[len(repositories)-1]) | |||
ctx.Resp.Header().Set("Link", fmt.Sprintf(`</v2/_catalog?%s>; rel="next"`, v.Encode())) | |||
} | |||
jsonResponse(ctx, http.StatusOK, RepositoryList{ | |||
Repositories: repositories, | |||
}) | |||
} | |||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#mounting-a-blob-from-another-repository | |||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post | |||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks |
@@ -267,6 +267,7 @@ func processImageManifestIndex(mci *manifestCreationInfo, buf *packages_module.H | |||
} | |||
func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, metadata *container_module.Metadata) (*packages_model.PackageVersion, error) { | |||
created := true | |||
p := &packages_model.Package{ | |||
OwnerID: mci.Owner.ID, | |||
Type: packages_model.TypeContainer, | |||
@@ -275,12 +276,21 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met | |||
} | |||
var err error | |||
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil { | |||
if err != packages_model.ErrDuplicatePackage { | |||
if err == packages_model.ErrDuplicatePackage { | |||
created = false | |||
} else { | |||
log.Error("Error inserting package: %v", err) | |||
return nil, err | |||
} | |||
} | |||
if created { | |||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository, strings.ToLower(mci.Owner.LowerName+"/"+mci.Image)); err != nil { | |||
log.Error("Error setting package property: %v", err) | |||
return nil, err | |||
} | |||
} | |||
metadata.IsTagged = mci.IsTagged | |||
metadataJSON, err := json.Marshal(metadata) |
@@ -25,7 +25,7 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac | |||
for _, pd := range pds { | |||
versions[pd.SemVer.String()] = createPackageMetadataVersion(registryURL, pd) | |||
for _, pvp := range pd.Properties { | |||
for _, pvp := range pd.VersionProperties { | |||
if pvp.Name == npm_module.TagProperty { | |||
distTags[pvp.Value] = pd.Version.Version | |||
} |
@@ -24,6 +24,7 @@ import ( | |||
user_setting "code.gitea.io/gitea/routers/web/user/setting" | |||
"code.gitea.io/gitea/services/forms" | |||
"code.gitea.io/gitea/services/org" | |||
container_service "code.gitea.io/gitea/services/packages/container" | |||
repo_service "code.gitea.io/gitea/services/repository" | |||
user_service "code.gitea.io/gitea/services/user" | |||
) | |||
@@ -88,6 +89,12 @@ func SettingsPost(ctx *context.Context) { | |||
} | |||
return | |||
} | |||
if err := container_service.UpdateRepositoryNames(ctx, org.AsUser(), form.Name); err != nil { | |||
ctx.ServerError("UpdateRepositoryNames", err) | |||
return | |||
} | |||
// reset ctx.org.OrgLink with new name | |||
ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(form.Name) | |||
log.Trace("Organization name changed: %s -> %s", org.Name, form.Name) |
@@ -30,6 +30,7 @@ import ( | |||
"code.gitea.io/gitea/modules/web/middleware" | |||
"code.gitea.io/gitea/services/agit" | |||
"code.gitea.io/gitea/services/forms" | |||
container_service "code.gitea.io/gitea/services/packages/container" | |||
user_service "code.gitea.io/gitea/services/user" | |||
) | |||
@@ -90,6 +91,11 @@ func HandleUsernameChange(ctx *context.Context, user *user_model.User, newName s | |||
return err | |||
} | |||
if err := container_service.UpdateRepositoryNames(ctx, user, newName); err != nil { | |||
ctx.ServerError("UpdateRepositoryNames", err) | |||
return err | |||
} | |||
log.Trace("User name changed: %s -> %s", user.Name, newName) | |||
return nil | |||
} |
@@ -6,10 +6,13 @@ package container | |||
import ( | |||
"context" | |||
"strings" | |||
"time" | |||
packages_model "code.gitea.io/gitea/models/packages" | |||
container_model "code.gitea.io/gitea/models/packages/container" | |||
user_model "code.gitea.io/gitea/models/user" | |||
container_module "code.gitea.io/gitea/modules/packages/container" | |||
"code.gitea.io/gitea/modules/util" | |||
) | |||
@@ -78,3 +81,25 @@ func cleanupExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) e | |||
return nil | |||
} | |||
// UpdateRepositoryNames updates the repository name property for all packages of the specific owner | |||
func UpdateRepositoryNames(ctx context.Context, owner *user_model.User, newOwnerName string) error { | |||
ps, err := packages_model.GetPackagesByType(ctx, owner.ID, packages_model.TypeContainer) | |||
if err != nil { | |||
return err | |||
} | |||
newOwnerName = strings.ToLower(newOwnerName) | |||
for _, p := range ps { | |||
if err := packages_model.DeletePropertyByName(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository); err != nil { | |||
return err | |||
} | |||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository, newOwnerName+"/"+p.LowerName); err != nil { | |||
return err | |||
} | |||
} | |||
return nil | |||
} |
@@ -34,10 +34,11 @@ type PackageInfo struct { | |||
// PackageCreationInfo describes a package to create | |||
type PackageCreationInfo struct { | |||
PackageInfo | |||
SemverCompatible bool | |||
Creator *user_model.User | |||
Metadata interface{} | |||
Properties map[string]string | |||
SemverCompatible bool | |||
Creator *user_model.User | |||
Metadata interface{} | |||
PackageProperties map[string]string | |||
VersionProperties map[string]string | |||
} | |||
// PackageFileInfo describes a package file | |||
@@ -110,8 +111,9 @@ func createPackageAndAddFile(pvci *PackageCreationInfo, pfci *PackageFileCreatio | |||
} | |||
func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, allowDuplicate bool) (*packages_model.PackageVersion, bool, error) { | |||
log.Trace("Creating package: %v, %v, %v, %s, %s, %+v, %v", pvci.Creator.ID, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version, pvci.Properties, allowDuplicate) | |||
log.Trace("Creating package: %v, %v, %v, %s, %s, %+v, %+v, %v", pvci.Creator.ID, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version, pvci.PackageProperties, pvci.VersionProperties, allowDuplicate) | |||
packageCreated := true | |||
p := &packages_model.Package{ | |||
OwnerID: pvci.Owner.ID, | |||
Type: pvci.PackageType, | |||
@@ -121,18 +123,29 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all | |||
} | |||
var err error | |||
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil { | |||
if err != packages_model.ErrDuplicatePackage { | |||
if err == packages_model.ErrDuplicatePackage { | |||
packageCreated = false | |||
} else { | |||
log.Error("Error inserting package: %v", err) | |||
return nil, false, err | |||
} | |||
} | |||
if packageCreated { | |||
for name, value := range pvci.PackageProperties { | |||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, name, value); err != nil { | |||
log.Error("Error setting package property: %v", err) | |||
return nil, false, err | |||
} | |||
} | |||
} | |||
metadataJSON, err := json.Marshal(pvci.Metadata) | |||
if err != nil { | |||
return nil, false, err | |||
} | |||
created := true | |||
versionCreated := true | |||
pv := &packages_model.PackageVersion{ | |||
PackageID: p.ID, | |||
CreatorID: pvci.Creator.ID, | |||
@@ -142,7 +155,7 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all | |||
} | |||
if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil { | |||
if err == packages_model.ErrDuplicatePackageVersion { | |||
created = false | |||
versionCreated = false | |||
} | |||
if err != packages_model.ErrDuplicatePackageVersion || !allowDuplicate { | |||
log.Error("Error inserting package: %v", err) | |||
@@ -150,8 +163,8 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all | |||
} | |||
} | |||
if created { | |||
for name, value := range pvci.Properties { | |||
if versionCreated { | |||
for name, value := range pvci.VersionProperties { | |||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, name, value); err != nil { | |||
log.Error("Error setting package version property: %v", err) | |||
return nil, false, err | |||
@@ -159,7 +172,7 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all | |||
} | |||
} | |||
return pv, created, nil | |||
return pv, versionCreated, nil | |||
} | |||
// AddFileToExistingPackage adds a file to an existing package. If the package does not exist, ErrPackageNotExist is returned | |||
@@ -350,9 +363,18 @@ func Cleanup(unused context.Context, olderThan time.Duration) error { | |||
return err | |||
} | |||
if err := packages_model.DeletePackagesIfUnreferenced(ctx); err != nil { | |||
ps, err := packages_model.FindUnreferencedPackages(ctx) | |||
if err != nil { | |||
return err | |||
} | |||
for _, p := range ps { | |||
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypePackage, p.ID); err != nil { | |||
return err | |||
} | |||
if err := packages_model.DeletePackageByID(ctx, p.ID); err != nil { | |||
return err | |||
} | |||
} | |||
pbs, err := packages_model.FindExpiredUnreferencedBlobs(ctx, olderThan) | |||
if err != nil { |