diff options
author | Daniel Baumann <daniel@debian.org> | 2024-10-18 20:33:49 +0200 |
---|---|---|
committer | Daniel Baumann <daniel@debian.org> | 2024-12-12 23:57:56 +0100 |
commit | e68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch) | |
tree | 97775d6c13b0f416af55314eb6a89ef792474615 /models/packages | |
parent | Initial commit. (diff) | |
download | forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip |
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to '')
-rw-r--r-- | models/packages/alpine/search.go | 53 | ||||
-rw-r--r-- | models/packages/conan/references.go | 170 | ||||
-rw-r--r-- | models/packages/conan/search.go | 149 | ||||
-rw-r--r-- | models/packages/conda/search.go | 63 | ||||
-rw-r--r-- | models/packages/container/const.go | 9 | ||||
-rw-r--r-- | models/packages/container/search.go | 285 | ||||
-rw-r--r-- | models/packages/cran/search.go | 90 | ||||
-rw-r--r-- | models/packages/debian/search.go | 157 | ||||
-rw-r--r-- | models/packages/debian/search_test.go | 93 | ||||
-rw-r--r-- | models/packages/descriptor.go | 260 | ||||
-rw-r--r-- | models/packages/nuget/search.go | 70 | ||||
-rw-r--r-- | models/packages/package.go | 351 | ||||
-rw-r--r-- | models/packages/package_blob.go | 154 | ||||
-rw-r--r-- | models/packages/package_blob_upload.go | 79 | ||||
-rw-r--r-- | models/packages/package_cleanup_rule.go | 109 | ||||
-rw-r--r-- | models/packages/package_file.go | 232 | ||||
-rw-r--r-- | models/packages/package_property.go | 121 | ||||
-rw-r--r-- | models/packages/package_test.go | 319 | ||||
-rw-r--r-- | models/packages/package_version.go | 348 | ||||
-rw-r--r-- | models/packages/rpm/search.go | 23 |
20 files changed, 3135 insertions, 0 deletions
diff --git a/models/packages/alpine/search.go b/models/packages/alpine/search.go new file mode 100644 index 0000000..77eccb9 --- /dev/null +++ b/models/packages/alpine/search.go @@ -0,0 +1,53 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package alpine + +import ( + "context" + + packages_model "code.gitea.io/gitea/models/packages" + alpine_module "code.gitea.io/gitea/modules/packages/alpine" +) + +// GetBranches gets all available branches +func GetBranches(ctx context.Context, ownerID int64) ([]string, error) { + return packages_model.GetDistinctPropertyValues( + ctx, + packages_model.TypeAlpine, + ownerID, + packages_model.PropertyTypeFile, + alpine_module.PropertyBranch, + nil, + ) +} + +// GetRepositories gets all available repositories for the given branch +func GetRepositories(ctx context.Context, ownerID int64, branch string) ([]string, error) { + return packages_model.GetDistinctPropertyValues( + ctx, + packages_model.TypeAlpine, + ownerID, + packages_model.PropertyTypeFile, + alpine_module.PropertyRepository, + &packages_model.DistinctPropertyDependency{ + Name: alpine_module.PropertyBranch, + Value: branch, + }, + ) +} + +// GetArchitectures gets all available architectures for the given repository +func GetArchitectures(ctx context.Context, ownerID int64, repository string) ([]string, error) { + return packages_model.GetDistinctPropertyValues( + ctx, + packages_model.TypeAlpine, + ownerID, + packages_model.PropertyTypeFile, + alpine_module.PropertyArchitecture, + &packages_model.DistinctPropertyDependency{ + Name: alpine_module.PropertyRepository, + Value: repository, + }, + ) +} diff --git a/models/packages/conan/references.go b/models/packages/conan/references.go new file mode 100644 index 0000000..0d888a1 --- /dev/null +++ b/models/packages/conan/references.go @@ -0,0 +1,170 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package conan + +import ( + "context" + "strconv" + "strings" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/packages" + conan_module "code.gitea.io/gitea/modules/packages/conan" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +var ( + ErrRecipeReferenceNotExist = util.NewNotExistErrorf("recipe reference does not exist") + ErrPackageReferenceNotExist = util.NewNotExistErrorf("package reference does not exist") +) + +// RecipeExists checks if a recipe exists +func RecipeExists(ctx context.Context, ownerID int64, ref *conan_module.RecipeReference) (bool, error) { + revisions, err := GetRecipeRevisions(ctx, ownerID, ref) + if err != nil { + return false, err + } + + return len(revisions) != 0, nil +} + +type PropertyValue struct { + Value string + CreatedUnix timeutil.TimeStamp +} + +func findPropertyValues(ctx context.Context, propertyName string, ownerID int64, name, version string, propertyFilter map[string]string) ([]*PropertyValue, error) { + var propsCond builder.Cond = builder.Eq{ + "package_property.ref_type": packages.PropertyTypeFile, + } + propsCond = propsCond.And(builder.Expr("package_property.ref_id = package_file.id")) + + propsCondBlock := builder.NewCond() + for name, value := range propertyFilter { + propsCondBlock = propsCondBlock.Or(builder.Eq{ + "package_property.name": name, + "package_property.value": value, + }) + } + propsCond = propsCond.And(propsCondBlock) + + var cond builder.Cond = builder.Eq{ + "package.type": packages.TypeConan, + "package.owner_id": ownerID, + "package.lower_name": strings.ToLower(name), + "package_version.lower_version": strings.ToLower(version), + "package_version.is_internal": false, + strconv.Itoa(len(propertyFilter)): builder.Select("COUNT(*)").Where(propsCond).From("package_property"), + } + + in2 := builder. + Select("package_file.id"). + From("package_file"). + InnerJoin("package_version", "package_version.id = package_file.version_id"). + InnerJoin("package", "package.id = package_version.package_id"). + Where(cond) + + query := builder. + Select("package_property.value, MAX(package_file.created_unix) AS created_unix"). + From("package_property"). + InnerJoin("package_file", "package_file.id = package_property.ref_id"). + Where(builder.Eq{"package_property.name": propertyName}.And(builder.In("package_property.ref_id", in2))). + GroupBy("package_property.value"). + OrderBy("created_unix DESC") + + var values []*PropertyValue + return values, db.GetEngine(ctx).SQL(query).Find(&values) +} + +// GetRecipeRevisions gets all revisions of a recipe +func GetRecipeRevisions(ctx context.Context, ownerID int64, ref *conan_module.RecipeReference) ([]*PropertyValue, error) { + values, err := findPropertyValues( + ctx, + conan_module.PropertyRecipeRevision, + ownerID, + ref.Name, + ref.Version, + map[string]string{ + conan_module.PropertyRecipeUser: ref.User, + conan_module.PropertyRecipeChannel: ref.Channel, + }, + ) + if err != nil { + return nil, err + } + + return values, nil +} + +// GetLastRecipeRevision gets the latest recipe revision +func GetLastRecipeRevision(ctx context.Context, ownerID int64, ref *conan_module.RecipeReference) (*PropertyValue, error) { + revisions, err := GetRecipeRevisions(ctx, ownerID, ref) + if err != nil { + return nil, err + } + + if len(revisions) == 0 { + return nil, ErrRecipeReferenceNotExist + } + return revisions[0], nil +} + +// GetPackageReferences gets all package references of a recipe +func GetPackageReferences(ctx context.Context, ownerID int64, ref *conan_module.RecipeReference) ([]*PropertyValue, error) { + values, err := findPropertyValues( + ctx, + conan_module.PropertyPackageReference, + ownerID, + ref.Name, + ref.Version, + map[string]string{ + conan_module.PropertyRecipeUser: ref.User, + conan_module.PropertyRecipeChannel: ref.Channel, + conan_module.PropertyRecipeRevision: ref.Revision, + }, + ) + if err != nil { + return nil, err + } + + return values, nil +} + +// GetPackageRevisions gets all revision of a package +func GetPackageRevisions(ctx context.Context, ownerID int64, ref *conan_module.PackageReference) ([]*PropertyValue, error) { + values, err := findPropertyValues( + ctx, + conan_module.PropertyPackageRevision, + ownerID, + ref.Recipe.Name, + ref.Recipe.Version, + map[string]string{ + conan_module.PropertyRecipeUser: ref.Recipe.User, + conan_module.PropertyRecipeChannel: ref.Recipe.Channel, + conan_module.PropertyRecipeRevision: ref.Recipe.Revision, + conan_module.PropertyPackageReference: ref.Reference, + }, + ) + if err != nil { + return nil, err + } + + return values, nil +} + +// GetLastPackageRevision gets the latest package revision +func GetLastPackageRevision(ctx context.Context, ownerID int64, ref *conan_module.PackageReference) (*PropertyValue, error) { + revisions, err := GetPackageRevisions(ctx, ownerID, ref) + if err != nil { + return nil, err + } + + if len(revisions) == 0 { + return nil, ErrPackageReferenceNotExist + } + return revisions[0], nil +} diff --git a/models/packages/conan/search.go b/models/packages/conan/search.go new file mode 100644 index 0000000..ab0bff5 --- /dev/null +++ b/models/packages/conan/search.go @@ -0,0 +1,149 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package conan + +import ( + "context" + "fmt" + "strconv" + "strings" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/container" + conan_module "code.gitea.io/gitea/modules/packages/conan" + + "xorm.io/builder" +) + +// buildCondition creates a Like condition if a wildcard is present. Otherwise Eq is used. +func buildCondition(name, value string) builder.Cond { + if strings.Contains(value, "*") { + return builder.Like{name, strings.ReplaceAll(strings.ReplaceAll(value, "_", "\\_"), "*", "%")} + } + return builder.Eq{name: value} +} + +type RecipeSearchOptions struct { + OwnerID int64 + Name string + Version string + User string + Channel string +} + +// SearchRecipes gets all recipes matching the search options +func SearchRecipes(ctx context.Context, opts *RecipeSearchOptions) ([]string, error) { + var cond builder.Cond = builder.Eq{ + "package_file.is_lead": true, + "package.type": packages.TypeConan, + "package.owner_id": opts.OwnerID, + "package_version.is_internal": false, + } + + if opts.Name != "" { + cond = cond.And(buildCondition("package.lower_name", strings.ToLower(opts.Name))) + } + if opts.Version != "" { + cond = cond.And(buildCondition("package_version.lower_version", strings.ToLower(opts.Version))) + } + if opts.User != "" || opts.Channel != "" { + var propsCond builder.Cond = builder.Eq{ + "package_property.ref_type": packages.PropertyTypeFile, + } + propsCond = propsCond.And(builder.Expr("package_property.ref_id = package_file.id")) + + count := 0 + propsCondBlock := builder.NewCond() + if opts.User != "" { + count++ + propsCondBlock = propsCondBlock.Or(builder.Eq{"package_property.name": conan_module.PropertyRecipeUser}.And(buildCondition("package_property.value", opts.User))) + } + if opts.Channel != "" { + count++ + propsCondBlock = propsCondBlock.Or(builder.Eq{"package_property.name": conan_module.PropertyRecipeChannel}.And(buildCondition("package_property.value", opts.Channel))) + } + propsCond = propsCond.And(propsCondBlock) + + cond = cond.And(builder.Eq{ + strconv.Itoa(count): builder.Select("COUNT(*)").Where(propsCond).From("package_property"), + }) + } + + query := builder. + Select("package.name, package_version.version, package_file.id"). + From("package_file"). + InnerJoin("package_version", "package_version.id = package_file.version_id"). + InnerJoin("package", "package.id = package_version.package_id"). + Where(cond) + + results := make([]struct { + Name string + Version string + ID int64 + }, 0, 5) + err := db.GetEngine(ctx).SQL(query).Find(&results) + if err != nil { + return nil, err + } + + unique := make(container.Set[string]) + for _, info := range results { + recipe := fmt.Sprintf("%s/%s", info.Name, info.Version) + + props, _ := packages.GetProperties(ctx, packages.PropertyTypeFile, info.ID) + if len(props) > 0 { + var ( + user = "" + channel = "" + ) + for _, prop := range props { + if prop.Name == conan_module.PropertyRecipeUser { + user = prop.Value + } + if prop.Name == conan_module.PropertyRecipeChannel { + channel = prop.Value + } + } + if user != "" && channel != "" { + recipe = fmt.Sprintf("%s@%s/%s", recipe, user, channel) + } + } + + unique.Add(recipe) + } + + recipes := make([]string, 0, len(unique)) + for recipe := range unique { + recipes = append(recipes, recipe) + } + return recipes, nil +} + +// GetPackageInfo gets the Conaninfo for a package +func GetPackageInfo(ctx context.Context, ownerID int64, ref *conan_module.PackageReference) (string, error) { + values, err := findPropertyValues( + ctx, + conan_module.PropertyPackageInfo, + ownerID, + ref.Recipe.Name, + ref.Recipe.Version, + map[string]string{ + conan_module.PropertyRecipeUser: ref.Recipe.User, + conan_module.PropertyRecipeChannel: ref.Recipe.Channel, + conan_module.PropertyRecipeRevision: ref.Recipe.Revision, + conan_module.PropertyPackageReference: ref.Reference, + conan_module.PropertyPackageRevision: ref.Revision, + }, + ) + if err != nil { + return "", err + } + + if len(values) == 0 { + return "", ErrPackageReferenceNotExist + } + + return values[0].Value, nil +} diff --git a/models/packages/conda/search.go b/models/packages/conda/search.go new file mode 100644 index 0000000..887441e --- /dev/null +++ b/models/packages/conda/search.go @@ -0,0 +1,63 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package conda + +import ( + "context" + "strings" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/packages" + conda_module "code.gitea.io/gitea/modules/packages/conda" + + "xorm.io/builder" +) + +type FileSearchOptions struct { + OwnerID int64 + Channel string + Subdir string + Filename string +} + +// SearchFiles gets all files matching the search options +func SearchFiles(ctx context.Context, opts *FileSearchOptions) ([]*packages.PackageFile, error) { + var cond builder.Cond = builder.Eq{ + "package.type": packages.TypeConda, + "package.owner_id": opts.OwnerID, + "package_version.is_internal": false, + } + + if opts.Filename != "" { + cond = cond.And(builder.Eq{ + "package_file.lower_name": strings.ToLower(opts.Filename), + }) + } + + var versionPropsCond builder.Cond = builder.Eq{ + "package_property.ref_type": packages.PropertyTypePackage, + "package_property.name": conda_module.PropertyChannel, + "package_property.value": opts.Channel, + } + + cond = cond.And(builder.In("package.id", builder.Select("package_property.ref_id").Where(versionPropsCond).From("package_property"))) + + var filePropsCond builder.Cond = builder.Eq{ + "package_property.ref_type": packages.PropertyTypeFile, + "package_property.name": conda_module.PropertySubdir, + "package_property.value": opts.Subdir, + } + + cond = cond.And(builder.In("package_file.id", builder.Select("package_property.ref_id").Where(filePropsCond).From("package_property"))) + + sess := db.GetEngine(ctx). + Select("package_file.*"). + Table("package_file"). + Join("INNER", "package_version", "package_version.id = package_file.version_id"). + Join("INNER", "package", "package.id = package_version.package_id"). + Where(cond) + + pfs := make([]*packages.PackageFile, 0, 10) + return pfs, sess.Find(&pfs) +} diff --git a/models/packages/container/const.go b/models/packages/container/const.go new file mode 100644 index 0000000..0dfbda0 --- /dev/null +++ b/models/packages/container/const.go @@ -0,0 +1,9 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package container + +const ( + ManifestFilename = "manifest.json" + UploadVersion = "_upload" +) diff --git a/models/packages/container/search.go b/models/packages/container/search.go new file mode 100644 index 0000000..5df3511 --- /dev/null +++ b/models/packages/container/search.go @@ -0,0 +1,285 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package container + +import ( + "context" + "strings" + "time" + + "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" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +var ErrContainerBlobNotExist = util.NewNotExistErrorf("container blob does not exist") + +type BlobSearchOptions struct { + OwnerID int64 + Image string + Digest string + Tag string + IsManifest bool + Repository string +} + +func (opts *BlobSearchOptions) toConds() builder.Cond { + var cond builder.Cond = builder.Eq{ + "package.type": packages.TypeContainer, + } + + if opts.OwnerID != 0 { + cond = cond.And(builder.Eq{"package.owner_id": opts.OwnerID}) + } + if opts.Image != "" { + cond = cond.And(builder.Eq{"package.lower_name": strings.ToLower(opts.Image)}) + } + if opts.Tag != "" { + cond = cond.And(builder.Eq{"package_version.lower_version": strings.ToLower(opts.Tag)}) + } + if opts.IsManifest { + cond = cond.And(builder.Eq{"package_file.lower_name": ManifestFilename}) + } + if opts.Digest != "" { + var propsCond builder.Cond = builder.Eq{ + "package_property.ref_type": packages.PropertyTypeFile, + "package_property.name": container_module.PropertyDigest, + "package_property.value": opts.Digest, + } + + cond = cond.And(builder.In("package_file.id", builder.Select("package_property.ref_id").Where(propsCond).From("package_property"))) + } + if opts.Repository != "" { + var propsCond builder.Cond = builder.Eq{ + "package_property.ref_type": packages.PropertyTypePackage, + "package_property.name": container_module.PropertyRepository, + "package_property.value": opts.Repository, + } + + cond = cond.And(builder.In("package.id", builder.Select("package_property.ref_id").Where(propsCond).From("package_property"))) + } + + return cond +} + +// GetContainerBlob gets the container blob matching the blob search options +// If multiple matching blobs are found (manifests with the same digest) the first (according to the database) is selected. +func GetContainerBlob(ctx context.Context, opts *BlobSearchOptions) (*packages.PackageFileDescriptor, error) { + pfds, err := getContainerBlobsLimit(ctx, opts, 1) + if err != nil { + return nil, err + } + if len(pfds) != 1 { + return nil, ErrContainerBlobNotExist + } + + return pfds[0], nil +} + +// GetContainerBlobs gets the container blobs matching the blob search options +func GetContainerBlobs(ctx context.Context, opts *BlobSearchOptions) ([]*packages.PackageFileDescriptor, error) { + return getContainerBlobsLimit(ctx, opts, 0) +} + +func getContainerBlobsLimit(ctx context.Context, opts *BlobSearchOptions, limit int) ([]*packages.PackageFileDescriptor, error) { + pfs := make([]*packages.PackageFile, 0, limit) + sess := db.GetEngine(ctx). + Join("INNER", "package_version", "package_version.id = package_file.version_id"). + Join("INNER", "package", "package.id = package_version.package_id"). + Where(opts.toConds()) + + if limit > 0 { + sess = sess.Limit(limit) + } + + if err := sess.Find(&pfs); err != nil { + return nil, err + } + + return packages.GetPackageFileDescriptors(ctx, pfs) +} + +// GetManifestVersions gets all package versions representing the matching manifest +func GetManifestVersions(ctx context.Context, opts *BlobSearchOptions) ([]*packages.PackageVersion, error) { + cond := opts.toConds().And(builder.Eq{"package_version.is_internal": false}) + + pvs := make([]*packages.PackageVersion, 0, 10) + return pvs, db.GetEngine(ctx). + Join("INNER", "package", "package.id = package_version.package_id"). + Join("INNER", "package_file", "package_file.version_id = package_version.id"). + Where(cond). + Find(&pvs) +} + +// GetImageTags gets a sorted list of the tags of an image +// The result is suitable for the api call. +func GetImageTags(ctx context.Context, ownerID int64, image string, n int, last string) ([]string, error) { + // Short circuit: n == 0 should return an empty list + if n == 0 { + return []string{}, nil + } + + var cond builder.Cond = builder.Eq{ + "package.type": packages.TypeContainer, + "package.owner_id": ownerID, + "package.lower_name": strings.ToLower(image), + "package_version.is_internal": false, + } + + var propsCond builder.Cond = builder.Eq{ + "package_property.ref_type": packages.PropertyTypeVersion, + "package_property.name": container_module.PropertyManifestTagged, + } + + cond = cond.And(builder.In("package_version.id", builder.Select("package_property.ref_id").Where(propsCond).From("package_property"))) + + if last != "" { + cond = cond.And(builder.Gt{"package_version.lower_version": strings.ToLower(last)}) + } + + sess := db.GetEngine(ctx). + Table("package_version"). + Select("package_version.lower_version"). + Join("INNER", "package", "package.id = package_version.package_id"). + Where(cond). + Asc("package_version.lower_version") + + var tags []string + if n > 0 { + sess = sess.Limit(n) + + tags = make([]string, 0, n) + } else { + tags = make([]string, 0, 10) + } + + return tags, sess.Find(&tags) +} + +type ImageTagsSearchOptions struct { + PackageID int64 + Query string + IsTagged bool + Sort packages.VersionSort + db.Paginator +} + +func (opts *ImageTagsSearchOptions) toConds() builder.Cond { + var cond builder.Cond = builder.Eq{ + "package.type": packages.TypeContainer, + "package.id": opts.PackageID, + "package_version.is_internal": false, + } + + if opts.Query != "" { + cond = cond.And(builder.Like{"package_version.lower_version", strings.ToLower(opts.Query)}) + } + + var propsCond builder.Cond = builder.Eq{ + "package_property.ref_type": packages.PropertyTypeVersion, + "package_property.name": container_module.PropertyManifestTagged, + } + + in := builder.In("package_version.id", builder.Select("package_property.ref_id").Where(propsCond).From("package_property")) + + if opts.IsTagged { + cond = cond.And(in) + } else { + cond = cond.And(builder.Not{in}) + } + + return cond +} + +func (opts *ImageTagsSearchOptions) configureOrderBy(e db.Engine) { + switch opts.Sort { + case packages.SortVersionDesc: + e.Desc("package_version.version") + case packages.SortVersionAsc: + e.Asc("package_version.version") + case packages.SortCreatedAsc: + e.Asc("package_version.created_unix") + default: + e.Desc("package_version.created_unix") + } + + // Sort by id for stable order with duplicates in the other field + e.Asc("package_version.id") +} + +// SearchImageTags gets a sorted list of the tags of an image +func SearchImageTags(ctx context.Context, opts *ImageTagsSearchOptions) ([]*packages.PackageVersion, int64, error) { + sess := db.GetEngine(ctx). + Join("INNER", "package", "package.id = package_version.package_id"). + Where(opts.toConds()) + + opts.configureOrderBy(sess) + + if opts.Paginator != nil { + sess = db.SetSessionPagination(sess, opts) + } + + pvs := make([]*packages.PackageVersion, 0, 10) + count, err := sess.FindAndCount(&pvs) + 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, + "package_version.lower_version": UploadVersion, + "package.type": packages.TypeContainer, + } + cond = cond.And(builder.Lt{"package_file.created_unix": time.Now().Add(-olderThan).Unix()}) + + var pfs []*packages.PackageFile + return pfs, db.GetEngine(ctx). + Join("INNER", "package_version", "package_version.id = package_file.version_id"). + Join("INNER", "package", "package.id = package_version.package_id"). + 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)}) + } + + if actor.IsGhost() { + actor = nil + } + + 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) +} diff --git a/models/packages/cran/search.go b/models/packages/cran/search.go new file mode 100644 index 0000000..8a8b52a --- /dev/null +++ b/models/packages/cran/search.go @@ -0,0 +1,90 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cran + +import ( + "context" + "strconv" + "strings" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/packages" + cran_module "code.gitea.io/gitea/modules/packages/cran" + + "xorm.io/builder" +) + +type SearchOptions struct { + OwnerID int64 + FileType string + Platform string + RVersion string + Filename string +} + +func (opts *SearchOptions) toConds() builder.Cond { + var cond builder.Cond = builder.Eq{ + "package.type": packages.TypeCran, + "package.owner_id": opts.OwnerID, + "package_version.is_internal": false, + } + + if opts.Filename != "" { + cond = cond.And(builder.Eq{"package_file.lower_name": strings.ToLower(opts.Filename)}) + } + + var propsCond builder.Cond = builder.Eq{ + "package_property.ref_type": packages.PropertyTypeFile, + } + propsCond = propsCond.And(builder.Expr("package_property.ref_id = package_file.id")) + + count := 1 + propsCondBlock := builder.Eq{"package_property.name": cran_module.PropertyType}.And(builder.Eq{"package_property.value": opts.FileType}) + + if opts.Platform != "" { + count += 2 + propsCondBlock = propsCondBlock. + Or(builder.Eq{"package_property.name": cran_module.PropertyPlatform}.And(builder.Eq{"package_property.value": opts.Platform})). + Or(builder.Eq{"package_property.name": cran_module.PropertyRVersion}.And(builder.Eq{"package_property.value": opts.RVersion})) + } + + propsCond = propsCond.And(propsCondBlock) + + cond = cond.And(builder.Eq{ + strconv.Itoa(count): builder.Select("COUNT(*)").Where(propsCond).From("package_property"), + }) + + return cond +} + +func SearchLatestVersions(ctx context.Context, opts *SearchOptions) ([]*packages.PackageVersion, error) { + sess := db.GetEngine(ctx). + Table("package_version"). + Select("package_version.*"). + Join("LEFT", "package_version pv2", builder.Expr("package_version.package_id = pv2.package_id AND pv2.is_internal = ? AND (package_version.created_unix < pv2.created_unix OR (package_version.created_unix = pv2.created_unix AND package_version.id < pv2.id))", false)). + Join("INNER", "package", "package.id = package_version.package_id"). + Join("INNER", "package_file", "package_file.version_id = package_version.id"). + Where(opts.toConds().And(builder.Expr("pv2.id IS NULL"))). + Asc("package.name") + + pvs := make([]*packages.PackageVersion, 0, 10) + return pvs, sess.Find(&pvs) +} + +func SearchFile(ctx context.Context, opts *SearchOptions) (*packages.PackageFile, error) { + sess := db.GetEngine(ctx). + Table("package_version"). + Select("package_file.*"). + Join("INNER", "package", "package.id = package_version.package_id"). + Join("INNER", "package_file", "package_file.version_id = package_version.id"). + Where(opts.toConds()) + + pf := &packages.PackageFile{} + if has, err := sess.Get(pf); err != nil { + return nil, err + } else if !has { + return nil, packages.ErrPackageFileNotExist + } + return pf, nil +} diff --git a/models/packages/debian/search.go b/models/packages/debian/search.go new file mode 100644 index 0000000..abf23e4 --- /dev/null +++ b/models/packages/debian/search.go @@ -0,0 +1,157 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package debian + +import ( + "context" + "strconv" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/packages" + debian_module "code.gitea.io/gitea/modules/packages/debian" + "code.gitea.io/gitea/modules/setting" + + "xorm.io/builder" +) + +type PackageSearchOptions struct { + OwnerID int64 + Distribution string + Component string + Architecture string +} + +func (opts *PackageSearchOptions) toCond() builder.Cond { + var cond builder.Cond = builder.Eq{ + "package_file.is_lead": true, + "package.type": packages.TypeDebian, + "package.owner_id": opts.OwnerID, + "package.is_internal": false, + "package_version.is_internal": false, + } + + props := make(map[string]string) + if opts.Distribution != "" { + props[debian_module.PropertyDistribution] = opts.Distribution + } + if opts.Component != "" { + props[debian_module.PropertyComponent] = opts.Component + } + if opts.Architecture != "" { + props[debian_module.PropertyArchitecture] = opts.Architecture + } + + if len(props) > 0 { + var propsCond builder.Cond = builder.Eq{ + "package_property.ref_type": packages.PropertyTypeFile, + } + propsCond = propsCond.And(builder.Expr("package_property.ref_id = package_file.id")) + + propsCondBlock := builder.NewCond() + for name, value := range props { + propsCondBlock = propsCondBlock.Or(builder.Eq{ + "package_property.name": name, + "package_property.value": value, + }) + } + propsCond = propsCond.And(propsCondBlock) + + cond = cond.And(builder.Eq{ + strconv.Itoa(len(props)): builder.Select("COUNT(*)").Where(propsCond).From("package_property"), + }) + } + + return cond +} + +// ExistPackages tests if there are packages matching the search options +func ExistPackages(ctx context.Context, opts *PackageSearchOptions) (bool, error) { + return db.GetEngine(ctx). + Table("package_file"). + Join("INNER", "package_version", "package_version.id = package_file.version_id"). + Join("INNER", "package", "package.id = package_version.package_id"). + Where(opts.toCond()). + Exist(new(packages.PackageFile)) +} + +// SearchPackages gets the packages matching the search options +func SearchPackages(ctx context.Context, opts *PackageSearchOptions, iter func(*packages.PackageFileDescriptor)) error { + var start int + batchSize := setting.Database.IterateBufferSize + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + beans := make([]*packages.PackageFile, 0, batchSize) + + if err := db.GetEngine(ctx). + Table("package_file"). + Select("package_file.*"). + Join("INNER", "package_version", "package_version.id = package_file.version_id"). + Join("INNER", "package", "package.id = package_version.package_id"). + Where(opts.toCond()). + Asc("package.lower_name", "package_version.created_unix"). + Limit(batchSize, start). + Find(&beans); err != nil { + return err + } + if len(beans) == 0 { + return nil + } + start += len(beans) + + for _, bean := range beans { + pfd, err := packages.GetPackageFileDescriptor(ctx, bean) + if err != nil { + return err + } + + iter(pfd) + } + } + } +} + +// GetDistributions gets all available distributions +func GetDistributions(ctx context.Context, ownerID int64) ([]string, error) { + return packages.GetDistinctPropertyValues( + ctx, + packages.TypeDebian, + ownerID, + packages.PropertyTypeFile, + debian_module.PropertyDistribution, + nil, + ) +} + +// GetComponents gets all available components for the given distribution +func GetComponents(ctx context.Context, ownerID int64, distribution string) ([]string, error) { + return packages.GetDistinctPropertyValues( + ctx, + packages.TypeDebian, + ownerID, + packages.PropertyTypeFile, + debian_module.PropertyComponent, + &packages.DistinctPropertyDependency{ + Name: debian_module.PropertyDistribution, + Value: distribution, + }, + ) +} + +// GetArchitectures gets all available architectures for the given distribution +func GetArchitectures(ctx context.Context, ownerID int64, distribution string) ([]string, error) { + return packages.GetDistinctPropertyValues( + ctx, + packages.TypeDebian, + ownerID, + packages.PropertyTypeFile, + debian_module.PropertyArchitecture, + &packages.DistinctPropertyDependency{ + Name: debian_module.PropertyDistribution, + Value: distribution, + }, + ) +} diff --git a/models/packages/debian/search_test.go b/models/packages/debian/search_test.go new file mode 100644 index 0000000..104a014 --- /dev/null +++ b/models/packages/debian/search_test.go @@ -0,0 +1,93 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package debian + +import ( + "strings" + "testing" + + "code.gitea.io/gitea/models/db" + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/packages" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + packages_service "code.gitea.io/gitea/services/packages" + + _ "code.gitea.io/gitea/models" + _ "code.gitea.io/gitea/models/actions" + _ "code.gitea.io/gitea/models/activities" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m) +} + +func preparePackage(t *testing.T, owner *user_model.User, name string) { + t.Helper() + + data, err := packages.CreateHashedBufferFromReader(strings.NewReader("data")) + require.NoError(t, err) + + _, _, err = packages_service.CreatePackageOrAddFileToExisting( + db.DefaultContext, + &packages_service.PackageCreationInfo{ + PackageInfo: packages_service.PackageInfo{ + Owner: owner, + PackageType: packages_model.TypeDebian, + Name: name, + }, + Creator: owner, + }, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: name, + }, + Data: data, + Creator: owner, + IsLead: true, + }, + ) + + require.NoError(t, err) +} + +func TestSearchPackages(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + defer test.MockVariableValue(&setting.Database.IterateBufferSize, 1)() + + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) + + preparePackage(t, user2, "debian-1") + preparePackage(t, user2, "debian-2") + preparePackage(t, user3, "debian-1") + + packageFiles := []string{} + require.NoError(t, SearchPackages(db.DefaultContext, &PackageSearchOptions{ + OwnerID: user2.ID, + }, func(pfd *packages_model.PackageFileDescriptor) { + assert.NotNil(t, pfd) + packageFiles = append(packageFiles, pfd.File.Name) + })) + + assert.Len(t, packageFiles, 2) + assert.Contains(t, packageFiles, "debian-1") + assert.Contains(t, packageFiles, "debian-2") + + packageFiles = []string{} + require.NoError(t, SearchPackages(db.DefaultContext, &PackageSearchOptions{ + OwnerID: user3.ID, + }, func(pfd *packages_model.PackageFileDescriptor) { + assert.NotNil(t, pfd) + packageFiles = append(packageFiles, pfd.File.Name) + })) + + assert.Len(t, packageFiles, 1) + assert.Contains(t, packageFiles, "debian-1") +} diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go new file mode 100644 index 0000000..803b73c --- /dev/null +++ b/models/packages/descriptor.go @@ -0,0 +1,260 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package packages + +import ( + "context" + "errors" + "fmt" + "net/url" + + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/packages/alpine" + "code.gitea.io/gitea/modules/packages/arch" + "code.gitea.io/gitea/modules/packages/cargo" + "code.gitea.io/gitea/modules/packages/chef" + "code.gitea.io/gitea/modules/packages/composer" + "code.gitea.io/gitea/modules/packages/conan" + "code.gitea.io/gitea/modules/packages/conda" + "code.gitea.io/gitea/modules/packages/container" + "code.gitea.io/gitea/modules/packages/cran" + "code.gitea.io/gitea/modules/packages/debian" + "code.gitea.io/gitea/modules/packages/helm" + "code.gitea.io/gitea/modules/packages/maven" + "code.gitea.io/gitea/modules/packages/npm" + "code.gitea.io/gitea/modules/packages/nuget" + "code.gitea.io/gitea/modules/packages/pub" + "code.gitea.io/gitea/modules/packages/pypi" + "code.gitea.io/gitea/modules/packages/rpm" + "code.gitea.io/gitea/modules/packages/rubygems" + "code.gitea.io/gitea/modules/packages/swift" + "code.gitea.io/gitea/modules/packages/vagrant" + "code.gitea.io/gitea/modules/util" + + "github.com/hashicorp/go-version" +) + +// PackagePropertyList is a list of package properties +type PackagePropertyList []*PackageProperty + +// GetByName gets the first property value with the specific name +func (l PackagePropertyList) GetByName(name string) string { + for _, pp := range l { + if pp.Name == name { + return pp.Value + } + } + return "" +} + +// 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 + PackageProperties PackagePropertyList + VersionProperties PackagePropertyList + Metadata any + Files []*PackageFileDescriptor +} + +// PackageFileDescriptor describes a package file +type PackageFileDescriptor struct { + File *PackageFile + Blob *PackageBlob + Properties PackagePropertyList +} + +// PackageWebLink returns the relative package web link +func (pd *PackageDescriptor) PackageWebLink() string { + return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HomeLink(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName)) +} + +// VersionWebLink returns the relative package version web link +func (pd *PackageDescriptor) VersionWebLink() string { + return fmt.Sprintf("%s/%s", pd.PackageWebLink(), url.PathEscape(pd.Version.LowerVersion)) +} + +// PackageHTMLURL returns the absolute package HTML URL +func (pd *PackageDescriptor) PackageHTMLURL() string { + return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HTMLURL(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName)) +} + +// VersionHTMLURL returns the absolute package version HTML URL +func (pd *PackageDescriptor) VersionHTMLURL() string { + return fmt.Sprintf("%s/%s", pd.PackageHTMLURL(), url.PathEscape(pd.Version.LowerVersion)) +} + +// CalculateBlobSize returns the total blobs size in bytes +func (pd *PackageDescriptor) CalculateBlobSize() int64 { + size := int64(0) + for _, f := range pd.Files { + size += f.Blob.Size + } + return size +} + +// GetPackageDescriptor gets the package description for a version +func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDescriptor, error) { + p, err := GetPackageByID(ctx, pv.PackageID) + if err != nil { + return nil, err + } + o, err := user_model.GetUserByID(ctx, p.OwnerID) + if err != nil { + return nil, err + } + repository, err := repo_model.GetRepositoryByID(ctx, p.RepoID) + if err != nil && !repo_model.IsErrRepoNotExist(err) { + return nil, err + } + creator, err := user_model.GetUserByID(ctx, pv.CreatorID) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + creator = user_model.NewGhostUser() + } else { + return nil, err + } + } + var semVer *version.Version + if p.SemverCompatible { + semVer, err = version.NewVersion(pv.Version) + if err != nil { + 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 + } + pfs, err := GetFilesByVersionID(ctx, pv.ID) + if err != nil { + return nil, err + } + + pfds, err := GetPackageFileDescriptors(ctx, pfs) + if err != nil { + return nil, err + } + + var metadata any + switch p.Type { + case TypeAlpine: + metadata = &alpine.VersionMetadata{} + case TypeArch: + metadata = &arch.VersionMetadata{} + case TypeCargo: + metadata = &cargo.Metadata{} + case TypeChef: + metadata = &chef.Metadata{} + case TypeComposer: + metadata = &composer.Metadata{} + case TypeConan: + metadata = &conan.Metadata{} + case TypeConda: + metadata = &conda.VersionMetadata{} + case TypeContainer: + metadata = &container.Metadata{} + case TypeCran: + metadata = &cran.Metadata{} + case TypeDebian: + metadata = &debian.Metadata{} + case TypeGeneric: + // generic packages have no metadata + case TypeGo: + // go packages have no metadata + case TypeHelm: + metadata = &helm.Metadata{} + case TypeNuGet: + metadata = &nuget.Metadata{} + case TypeNpm: + metadata = &npm.Metadata{} + case TypeMaven: + metadata = &maven.Metadata{} + case TypePub: + metadata = &pub.Metadata{} + case TypePyPI: + metadata = &pypi.Metadata{} + case TypeRpm: + metadata = &rpm.VersionMetadata{} + case TypeRubyGems: + metadata = &rubygems.Metadata{} + case TypeSwift: + metadata = &swift.Metadata{} + case TypeVagrant: + metadata = &vagrant.Metadata{} + default: + panic(fmt.Sprintf("unknown package type: %s", string(p.Type))) + } + if metadata != nil { + if err := json.Unmarshal([]byte(pv.MetadataJSON), &metadata); err != nil { + return nil, err + } + } + + return &PackageDescriptor{ + Package: p, + Owner: o, + Repository: repository, + Version: pv, + SemVer: semVer, + Creator: creator, + PackageProperties: PackagePropertyList(pps), + VersionProperties: PackagePropertyList(pvps), + Metadata: metadata, + Files: pfds, + }, nil +} + +// GetPackageFileDescriptor gets a package file descriptor for a package file +func GetPackageFileDescriptor(ctx context.Context, pf *PackageFile) (*PackageFileDescriptor, error) { + pb, err := GetBlobByID(ctx, pf.BlobID) + if err != nil { + return nil, err + } + pfps, err := GetProperties(ctx, PropertyTypeFile, pf.ID) + if err != nil { + return nil, err + } + return &PackageFileDescriptor{ + pf, + pb, + PackagePropertyList(pfps), + }, nil +} + +// GetPackageFileDescriptors gets the package file descriptors for the package files +func GetPackageFileDescriptors(ctx context.Context, pfs []*PackageFile) ([]*PackageFileDescriptor, error) { + pfds := make([]*PackageFileDescriptor, 0, len(pfs)) + for _, pf := range pfs { + pfd, err := GetPackageFileDescriptor(ctx, pf) + if err != nil { + return nil, err + } + pfds = append(pfds, pfd) + } + return pfds, nil +} + +// GetPackageDescriptors gets the package descriptions for the versions +func GetPackageDescriptors(ctx context.Context, pvs []*PackageVersion) ([]*PackageDescriptor, error) { + pds := make([]*PackageDescriptor, 0, len(pvs)) + for _, pv := range pvs { + pd, err := GetPackageDescriptor(ctx, pv) + if err != nil { + return nil, err + } + pds = append(pds, pd) + } + return pds, nil +} diff --git a/models/packages/nuget/search.go b/models/packages/nuget/search.go new file mode 100644 index 0000000..7a505ff --- /dev/null +++ b/models/packages/nuget/search.go @@ -0,0 +1,70 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package nuget + +import ( + "context" + "strings" + + "code.gitea.io/gitea/models/db" + packages_model "code.gitea.io/gitea/models/packages" + + "xorm.io/builder" +) + +// SearchVersions gets all versions of packages matching the search options +func SearchVersions(ctx context.Context, opts *packages_model.PackageSearchOptions) ([]*packages_model.PackageVersion, int64, error) { + cond := toConds(opts) + + e := db.GetEngine(ctx) + + total, err := e. + Where(cond). + Count(&packages_model.Package{}) + if err != nil { + return nil, 0, err + } + + inner := builder. + Dialect(db.BuilderDialect()). // builder needs the sql dialect to build the Limit() below + Select("*"). + From("package"). + Where(cond). + OrderBy("package.name ASC") + if opts.Paginator != nil { + skip, take := opts.GetSkipTake() + inner = inner.Limit(take, skip) + } + + sess := e. + Where(opts.ToConds()). + Table("package_version"). + Join("INNER", inner, "package.id = package_version.package_id") + + pvs := make([]*packages_model.PackageVersion, 0, 10) + return pvs, total, sess.Find(&pvs) +} + +// CountPackages counts all packages matching the search options +func CountPackages(ctx context.Context, opts *packages_model.PackageSearchOptions) (int64, error) { + return db.GetEngine(ctx). + Where(toConds(opts)). + Count(&packages_model.Package{}) +} + +func toConds(opts *packages_model.PackageSearchOptions) builder.Cond { + var cond builder.Cond = builder.Eq{ + "package.is_internal": opts.IsInternal.Value(), + "package.owner_id": opts.OwnerID, + "package.type": packages_model.TypeNuGet, + } + if opts.Name.Value != "" { + if opts.Name.ExactMatch { + cond = cond.And(builder.Eq{"package.lower_name": strings.ToLower(opts.Name.Value)}) + } else { + cond = cond.And(builder.Like{"package.lower_name", strings.ToLower(opts.Name.Value)}) + } + } + return cond +} diff --git a/models/packages/package.go b/models/packages/package.go new file mode 100644 index 0000000..364cc2e --- /dev/null +++ b/models/packages/package.go @@ -0,0 +1,351 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package packages + +import ( + "context" + "fmt" + "strings" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" + "xorm.io/xorm" +) + +func init() { + db.RegisterModel(new(Package)) +} + +var ( + // ErrDuplicatePackage indicates a duplicated package error + ErrDuplicatePackage = util.NewAlreadyExistErrorf("package already exists") + // ErrPackageNotExist indicates a package not exist error + ErrPackageNotExist = util.NewNotExistErrorf("package does not exist") +) + +// Type of a package +type Type string + +// List of supported packages +const ( + TypeAlpine Type = "alpine" + TypeArch Type = "arch" + TypeCargo Type = "cargo" + TypeChef Type = "chef" + TypeComposer Type = "composer" + TypeConan Type = "conan" + TypeConda Type = "conda" + TypeContainer Type = "container" + TypeCran Type = "cran" + TypeDebian Type = "debian" + TypeGeneric Type = "generic" + TypeGo Type = "go" + TypeHelm Type = "helm" + TypeMaven Type = "maven" + TypeNpm Type = "npm" + TypeNuGet Type = "nuget" + TypePub Type = "pub" + TypePyPI Type = "pypi" + TypeRpm Type = "rpm" + TypeRubyGems Type = "rubygems" + TypeSwift Type = "swift" + TypeVagrant Type = "vagrant" +) + +var TypeList = []Type{ + TypeAlpine, + TypeArch, + TypeCargo, + TypeChef, + TypeComposer, + TypeConan, + TypeConda, + TypeContainer, + TypeCran, + TypeDebian, + TypeGeneric, + TypeGo, + TypeHelm, + TypeMaven, + TypeNpm, + TypeNuGet, + TypePub, + TypePyPI, + TypeRpm, + TypeRubyGems, + TypeSwift, + TypeVagrant, +} + +// Name gets the name of the package type +func (pt Type) Name() string { + switch pt { + case TypeAlpine: + return "Alpine" + case TypeArch: + return "Arch" + case TypeCargo: + return "Cargo" + case TypeChef: + return "Chef" + case TypeComposer: + return "Composer" + case TypeConan: + return "Conan" + case TypeConda: + return "Conda" + case TypeContainer: + return "Container" + case TypeCran: + return "CRAN" + case TypeDebian: + return "Debian" + case TypeGeneric: + return "Generic" + case TypeGo: + return "Go" + case TypeHelm: + return "Helm" + case TypeMaven: + return "Maven" + case TypeNpm: + return "npm" + case TypeNuGet: + return "NuGet" + case TypePub: + return "Pub" + case TypePyPI: + return "PyPI" + case TypeRpm: + return "RPM" + case TypeRubyGems: + return "RubyGems" + case TypeSwift: + return "Swift" + case TypeVagrant: + return "Vagrant" + } + panic(fmt.Sprintf("unknown package type: %s", string(pt))) +} + +// SVGName gets the name of the package type svg image +func (pt Type) SVGName() string { + switch pt { + case TypeAlpine: + return "gitea-alpine" + case TypeArch: + return "gitea-arch" + case TypeCargo: + return "gitea-cargo" + case TypeChef: + return "gitea-chef" + case TypeComposer: + return "gitea-composer" + case TypeConan: + return "gitea-conan" + case TypeConda: + return "gitea-conda" + case TypeContainer: + return "octicon-container" + case TypeCran: + return "gitea-cran" + case TypeDebian: + return "gitea-debian" + case TypeGeneric: + return "octicon-package" + case TypeGo: + return "gitea-go" + case TypeHelm: + return "gitea-helm" + case TypeMaven: + return "gitea-maven" + case TypeNpm: + return "gitea-npm" + case TypeNuGet: + return "gitea-nuget" + case TypePub: + return "gitea-pub" + case TypePyPI: + return "gitea-python" + case TypeRpm: + return "gitea-rpm" + case TypeRubyGems: + return "gitea-rubygems" + case TypeSwift: + return "gitea-swift" + case TypeVagrant: + return "gitea-vagrant" + } + panic(fmt.Sprintf("unknown package type: %s", string(pt))) +} + +// Package represents a package +type Package struct { + ID int64 `xorm:"pk autoincr"` + OwnerID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + RepoID int64 `xorm:"INDEX"` + Type Type `xorm:"UNIQUE(s) INDEX NOT NULL"` + Name string `xorm:"NOT NULL"` + LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + SemverCompatible bool `xorm:"NOT NULL DEFAULT false"` + IsInternal bool `xorm:"NOT NULL DEFAULT false"` +} + +// TryInsertPackage inserts a package. If a package exists already, ErrDuplicatePackage is returned +func TryInsertPackage(ctx context.Context, p *Package) (*Package, error) { + e := db.GetEngine(ctx) + + existing := &Package{} + + has, err := e.Where(builder.Eq{ + "owner_id": p.OwnerID, + "type": p.Type, + "lower_name": p.LowerName, + }).Get(existing) + if err != nil { + return nil, err + } + if has { + return existing, ErrDuplicatePackage + } + if _, err = e.Insert(p); err != nil { + return nil, err + } + return p, nil +} + +// DeletePackageByID deletes a package by id +func DeletePackageByID(ctx context.Context, packageID int64) error { + n, err := db.GetEngine(ctx).ID(packageID).Delete(&Package{}) + if n == 0 && err == nil { + return ErrPackageNotExist + } + return err +} + +// SetRepositoryLink sets the linked repository +func SetRepositoryLink(ctx context.Context, packageID, repoID int64) error { + n, err := db.GetEngine(ctx).ID(packageID).Cols("repo_id").Update(&Package{RepoID: repoID}) + if n == 0 && err == nil { + return ErrPackageNotExist + } + return err +} + +// UnlinkRepositoryFromAllPackages unlinks every package from the repository +func UnlinkRepositoryFromAllPackages(ctx context.Context, repoID int64) error { + _, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Cols("repo_id").Update(&Package{}) + return err +} + +// GetPackageByID gets a package by id +func GetPackageByID(ctx context.Context, packageID int64) (*Package, error) { + p := &Package{} + + has, err := db.GetEngine(ctx).ID(packageID).Get(p) + if err != nil { + return nil, err + } + if !has { + return nil, ErrPackageNotExist + } + return p, nil +} + +// GetPackageByName gets a package by name +func GetPackageByName(ctx context.Context, ownerID int64, packageType Type, name string) (*Package, error) { + var cond builder.Cond = builder.Eq{ + "package.owner_id": ownerID, + "package.type": packageType, + "package.lower_name": strings.ToLower(name), + "package.is_internal": false, + } + + p := &Package{} + + has, err := db.GetEngine(ctx). + Where(cond). + Get(p) + if err != nil { + return nil, err + } + if !has { + return nil, ErrPackageNotExist + } + return p, nil +} + +// GetPackagesByType gets all packages of a specific type +func GetPackagesByType(ctx context.Context, ownerID int64, packageType Type) ([]*Package, error) { + var cond builder.Cond = builder.Eq{ + "package.owner_id": ownerID, + "package.type": packageType, + "package.is_internal": false, + } + + ps := make([]*Package, 0, 10) + return ps, db.GetEngine(ctx). + Where(cond). + Find(&ps) +} + +// FindUnreferencedPackages gets all packages without associated versions +func FindUnreferencedPackages(ctx context.Context) ([]int64, error) { + var pIDs []int64 + if err := db.GetEngine(ctx). + Select("package.id"). + Table("package"). + Join("LEFT", "package_version", "package_version.package_id = package.id"). + Where("package_version.id IS NULL"). + Find(&pIDs); err != nil { + return nil, err + } + return pIDs, nil +} + +func getPackages(ctx context.Context) *xorm.Session { + return db.GetEngine(ctx). + Table("package_version"). + Join("INNER", "package", "package.id = package_version.package_id"). + Where("package_version.is_internal = ?", false) +} + +func getOwnerPackages(ctx context.Context, ownerID int64) *xorm.Session { + return getPackages(ctx). + Where("package.owner_id = ?", ownerID) +} + +// HasOwnerPackages tests if a user/org has accessible packages +func HasOwnerPackages(ctx context.Context, ownerID int64) (bool, error) { + return getOwnerPackages(ctx, ownerID). + Exist(&Package{}) +} + +// CountOwnerPackages counts user/org accessible packages +func CountOwnerPackages(ctx context.Context, ownerID int64) (int64, error) { + return getOwnerPackages(ctx, ownerID). + Distinct("package.id"). + Count(&Package{}) +} + +func getRepositoryPackages(ctx context.Context, repositoryID int64) *xorm.Session { + return getPackages(ctx). + Where("package.repo_id = ?", repositoryID) +} + +// HasRepositoryPackages tests if a repository has packages +func HasRepositoryPackages(ctx context.Context, repositoryID int64) (bool, error) { + return getRepositoryPackages(ctx, repositoryID). + Exist(&PackageVersion{}) +} + +// CountRepositoryPackages counts packages of a repository +func CountRepositoryPackages(ctx context.Context, repositoryID int64) (int64, error) { + return getRepositoryPackages(ctx, repositoryID). + Distinct("package.id"). + Count(&Package{}) +} diff --git a/models/packages/package_blob.go b/models/packages/package_blob.go new file mode 100644 index 0000000..d9c30b6 --- /dev/null +++ b/models/packages/package_blob.go @@ -0,0 +1,154 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package packages + +import ( + "context" + "strconv" + "time" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +// ErrPackageBlobNotExist indicates a package blob not exist error +var ErrPackageBlobNotExist = util.NewNotExistErrorf("package blob does not exist") + +func init() { + db.RegisterModel(new(PackageBlob)) +} + +// PackageBlob represents a package blob +type PackageBlob struct { + ID int64 `xorm:"pk autoincr"` + Size int64 `xorm:"NOT NULL DEFAULT 0"` + HashMD5 string `xorm:"hash_md5 char(32) UNIQUE(md5) INDEX NOT NULL"` + HashSHA1 string `xorm:"hash_sha1 char(40) UNIQUE(sha1) INDEX NOT NULL"` + HashSHA256 string `xorm:"hash_sha256 char(64) UNIQUE(sha256) INDEX NOT NULL"` + HashSHA512 string `xorm:"hash_sha512 char(128) UNIQUE(sha512) INDEX NOT NULL"` + CreatedUnix timeutil.TimeStamp `xorm:"created INDEX NOT NULL"` +} + +// GetOrInsertBlob inserts a blob. If the blob exists already the existing blob is returned +func GetOrInsertBlob(ctx context.Context, pb *PackageBlob) (*PackageBlob, bool, error) { + e := db.GetEngine(ctx) + + existing := &PackageBlob{} + + has, err := e.Where(builder.Eq{ + "size": pb.Size, + "hash_md5": pb.HashMD5, + "hash_sha1": pb.HashSHA1, + "hash_sha256": pb.HashSHA256, + "hash_sha512": pb.HashSHA512, + }).Get(existing) + if err != nil { + return nil, false, err + } + if has { + return existing, true, nil + } + if _, err = e.Insert(pb); err != nil { + return nil, false, err + } + return pb, false, nil +} + +// GetBlobByID gets a blob by id +func GetBlobByID(ctx context.Context, blobID int64) (*PackageBlob, error) { + pb := &PackageBlob{} + + has, err := db.GetEngine(ctx).ID(blobID).Get(pb) + if err != nil { + return nil, err + } + if !has { + return nil, ErrPackageBlobNotExist + } + return pb, nil +} + +// ExistPackageBlobWithSHA returns if a package blob exists with the provided sha +func ExistPackageBlobWithSHA(ctx context.Context, blobSha256 string) (bool, error) { + return db.GetEngine(ctx).Exist(&PackageBlob{ + HashSHA256: blobSha256, + }) +} + +// FindExpiredUnreferencedBlobs gets all blobs without associated files older than the specific duration +func FindExpiredUnreferencedBlobs(ctx context.Context, olderThan time.Duration) ([]*PackageBlob, error) { + pbs := make([]*PackageBlob, 0, 10) + return pbs, db.GetEngine(ctx). + Table("package_blob"). + Join("LEFT", "package_file", "package_file.blob_id = package_blob.id"). + Where("package_file.id IS NULL AND package_blob.created_unix < ?", time.Now().Add(-olderThan).Unix()). + Find(&pbs) +} + +// DeleteBlobByID deletes a blob by id +func DeleteBlobByID(ctx context.Context, blobID int64) error { + _, err := db.GetEngine(ctx).ID(blobID).Delete(&PackageBlob{}) + return err +} + +// GetTotalBlobSize returns the total blobs size in bytes +func GetTotalBlobSize(ctx context.Context) (int64, error) { + return db.GetEngine(ctx). + SumInt(&PackageBlob{}, "size") +} + +// GetTotalUnreferencedBlobSize returns the total size of all unreferenced blobs in bytes +func GetTotalUnreferencedBlobSize(ctx context.Context) (int64, error) { + return db.GetEngine(ctx). + Table("package_blob"). + Join("LEFT", "package_file", "package_file.blob_id = package_blob.id"). + Where("package_file.id IS NULL"). + SumInt(&PackageBlob{}, "size") +} + +// IsBlobAccessibleForUser tests if the user has access to the blob +func IsBlobAccessibleForUser(ctx context.Context, blobID int64, user *user_model.User) (bool, error) { + if user.IsAdmin { + return true, nil + } + + maxTeamAuthorize := builder. + Select("max(team.authorize)"). + From("team"). + InnerJoin("team_user", "team_user.team_id = team.id"). + Where(builder.Eq{"team_user.uid": user.ID}.And(builder.Expr("team_user.org_id = `user`.id"))) + + maxTeamUnitAccessMode := builder. + Select("max(team_unit.access_mode)"). + From("team"). + InnerJoin("team_user", "team_user.team_id = team.id"). + InnerJoin("team_unit", "team_unit.team_id = team.id"). + Where(builder.Eq{"team_user.uid": user.ID, "team_unit.type": unit.TypePackages}.And(builder.Expr("team_user.org_id = `user`.id"))) + + cond := builder.Eq{"package_blob.id": blobID}.And( + // owner = user + builder.Eq{"`user`.id": user.ID}. + // user can see owner + Or(builder.Eq{"`user`.visibility": structs.VisibleTypePublic}.Or(builder.Eq{"`user`.visibility": structs.VisibleTypeLimited})). + // owner is an organization and user has access to it + Or(builder.Eq{"`user`.type": user_model.UserTypeOrganization}. + And(builder.Lte{strconv.Itoa(int(perm.AccessModeRead)): maxTeamAuthorize}.Or(builder.Lte{strconv.Itoa(int(perm.AccessModeRead)): maxTeamUnitAccessMode}))), + ) + + return db.GetEngine(ctx). + Table("package_blob"). + Join("INNER", "package_file", "package_file.blob_id = package_blob.id"). + Join("INNER", "package_version", "package_version.id = package_file.version_id"). + Join("INNER", "package", "package.id = package_version.package_id"). + Join("INNER", "user", "`user`.id = package.owner_id"). + Where(cond). + Exist(&PackageBlob{}) +} diff --git a/models/packages/package_blob_upload.go b/models/packages/package_blob_upload.go new file mode 100644 index 0000000..4b0e789 --- /dev/null +++ b/models/packages/package_blob_upload.go @@ -0,0 +1,79 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package packages + +import ( + "context" + "strings" + "time" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" +) + +// ErrPackageBlobUploadNotExist indicates a package blob upload not exist error +var ErrPackageBlobUploadNotExist = util.NewNotExistErrorf("package blob upload does not exist") + +func init() { + db.RegisterModel(new(PackageBlobUpload)) +} + +// PackageBlobUpload represents a package blob upload +type PackageBlobUpload struct { + ID string `xorm:"pk"` + BytesReceived int64 `xorm:"NOT NULL DEFAULT 0"` + HashStateBytes []byte `xorm:"BLOB"` + CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated INDEX NOT NULL"` +} + +// CreateBlobUpload inserts a blob upload +func CreateBlobUpload(ctx context.Context) (*PackageBlobUpload, error) { + id, err := util.CryptoRandomString(25) + if err != nil { + return nil, err + } + + pbu := &PackageBlobUpload{ + ID: strings.ToLower(id), + } + + _, err = db.GetEngine(ctx).Insert(pbu) + return pbu, err +} + +// GetBlobUploadByID gets a blob upload by id +func GetBlobUploadByID(ctx context.Context, id string) (*PackageBlobUpload, error) { + pbu := &PackageBlobUpload{} + + has, err := db.GetEngine(ctx).ID(id).Get(pbu) + if err != nil { + return nil, err + } + if !has { + return nil, ErrPackageBlobUploadNotExist + } + return pbu, nil +} + +// UpdateBlobUpload updates the blob upload +func UpdateBlobUpload(ctx context.Context, pbu *PackageBlobUpload) error { + _, err := db.GetEngine(ctx).ID(pbu.ID).Update(pbu) + return err +} + +// DeleteBlobUploadByID deletes the blob upload +func DeleteBlobUploadByID(ctx context.Context, id string) error { + _, err := db.GetEngine(ctx).ID(id).Delete(&PackageBlobUpload{}) + return err +} + +// FindExpiredBlobUploads gets all expired blob uploads +func FindExpiredBlobUploads(ctx context.Context, olderThan time.Duration) ([]*PackageBlobUpload, error) { + pbus := make([]*PackageBlobUpload, 0, 10) + return pbus, db.GetEngine(ctx). + Where("updated_unix < ?", time.Now().Add(-olderThan).Unix()). + Find(&pbus) +} diff --git a/models/packages/package_cleanup_rule.go b/models/packages/package_cleanup_rule.go new file mode 100644 index 0000000..fa12dec --- /dev/null +++ b/models/packages/package_cleanup_rule.go @@ -0,0 +1,109 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package packages + +import ( + "context" + "fmt" + "regexp" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +var ErrPackageCleanupRuleNotExist = util.NewNotExistErrorf("package blob does not exist") + +func init() { + db.RegisterModel(new(PackageCleanupRule)) +} + +// PackageCleanupRule represents a rule which describes when to clean up package versions +type PackageCleanupRule struct { + ID int64 `xorm:"pk autoincr"` + Enabled bool `xorm:"INDEX NOT NULL DEFAULT false"` + OwnerID int64 `xorm:"UNIQUE(s) INDEX NOT NULL DEFAULT 0"` + Type Type `xorm:"UNIQUE(s) INDEX NOT NULL"` + KeepCount int `xorm:"NOT NULL DEFAULT 0"` + KeepPattern string `xorm:"NOT NULL DEFAULT ''"` + KeepPatternMatcher *regexp.Regexp `xorm:"-"` + RemoveDays int `xorm:"NOT NULL DEFAULT 0"` + RemovePattern string `xorm:"NOT NULL DEFAULT ''"` + RemovePatternMatcher *regexp.Regexp `xorm:"-"` + MatchFullName bool `xorm:"NOT NULL DEFAULT false"` + CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL DEFAULT 0"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL DEFAULT 0"` +} + +func (pcr *PackageCleanupRule) CompiledPattern() error { + if pcr.KeepPatternMatcher != nil || pcr.RemovePatternMatcher != nil { + return nil + } + + if pcr.KeepPattern != "" { + var err error + pcr.KeepPatternMatcher, err = regexp.Compile(fmt.Sprintf(`(?i)\A%s\z`, pcr.KeepPattern)) + if err != nil { + return err + } + } + + if pcr.RemovePattern != "" { + var err error + pcr.RemovePatternMatcher, err = regexp.Compile(fmt.Sprintf(`(?i)\A%s\z`, pcr.RemovePattern)) + if err != nil { + return err + } + } + + return nil +} + +func InsertCleanupRule(ctx context.Context, pcr *PackageCleanupRule) (*PackageCleanupRule, error) { + return pcr, db.Insert(ctx, pcr) +} + +func GetCleanupRuleByID(ctx context.Context, id int64) (*PackageCleanupRule, error) { + pcr := &PackageCleanupRule{} + + has, err := db.GetEngine(ctx).ID(id).Get(pcr) + if err != nil { + return nil, err + } + if !has { + return nil, ErrPackageCleanupRuleNotExist + } + return pcr, nil +} + +func UpdateCleanupRule(ctx context.Context, pcr *PackageCleanupRule) error { + _, err := db.GetEngine(ctx).ID(pcr.ID).AllCols().Update(pcr) + return err +} + +func GetCleanupRulesByOwner(ctx context.Context, ownerID int64) ([]*PackageCleanupRule, error) { + pcrs := make([]*PackageCleanupRule, 0, 10) + return pcrs, db.GetEngine(ctx).Where("owner_id = ?", ownerID).Find(&pcrs) +} + +func DeleteCleanupRuleByID(ctx context.Context, ruleID int64) error { + _, err := db.GetEngine(ctx).ID(ruleID).Delete(&PackageCleanupRule{}) + return err +} + +func HasOwnerCleanupRuleForPackageType(ctx context.Context, ownerID int64, packageType Type) (bool, error) { + return db.GetEngine(ctx). + Where("owner_id = ? AND type = ?", ownerID, packageType). + Exist(&PackageCleanupRule{}) +} + +func IterateEnabledCleanupRules(ctx context.Context, callback func(context.Context, *PackageCleanupRule) error) error { + return db.Iterate( + ctx, + builder.Eq{"enabled": true}, + callback, + ) +} diff --git a/models/packages/package_file.go b/models/packages/package_file.go new file mode 100644 index 0000000..1bb6b57 --- /dev/null +++ b/models/packages/package_file.go @@ -0,0 +1,232 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package packages + +import ( + "context" + "strconv" + "strings" + "time" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +func init() { + db.RegisterModel(new(PackageFile)) +} + +var ( + // ErrDuplicatePackageFile indicates a duplicated package file error + ErrDuplicatePackageFile = util.NewAlreadyExistErrorf("package file already exists") + // ErrPackageFileNotExist indicates a package file not exist error + ErrPackageFileNotExist = util.NewNotExistErrorf("package file does not exist") +) + +// EmptyFileKey is a named constant for an empty file key +const EmptyFileKey = "" + +// PackageFile represents a package file +type PackageFile struct { + ID int64 `xorm:"pk autoincr"` + VersionID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + BlobID int64 `xorm:"INDEX NOT NULL"` + Name string `xorm:"NOT NULL"` + LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + CompositeKey string `xorm:"UNIQUE(s) INDEX"` + IsLead bool `xorm:"NOT NULL DEFAULT false"` + CreatedUnix timeutil.TimeStamp `xorm:"created INDEX NOT NULL"` +} + +// TryInsertFile inserts a file. If the file exists already ErrDuplicatePackageFile is returned +func TryInsertFile(ctx context.Context, pf *PackageFile) (*PackageFile, error) { + e := db.GetEngine(ctx) + + existing := &PackageFile{} + + has, err := e.Where(builder.Eq{ + "version_id": pf.VersionID, + "lower_name": pf.LowerName, + "composite_key": pf.CompositeKey, + }).Get(existing) + if err != nil { + return nil, err + } + if has { + return existing, ErrDuplicatePackageFile + } + if _, err = e.Insert(pf); err != nil { + return nil, err + } + return pf, nil +} + +// GetFilesByVersionID gets all files of a version +func GetFilesByVersionID(ctx context.Context, versionID int64) ([]*PackageFile, error) { + pfs := make([]*PackageFile, 0, 10) + return pfs, db.GetEngine(ctx).Where("version_id = ?", versionID).Find(&pfs) +} + +// GetFileForVersionByID gets a file of a version by id +func GetFileForVersionByID(ctx context.Context, versionID, fileID int64) (*PackageFile, error) { + pf := &PackageFile{ + VersionID: versionID, + } + + has, err := db.GetEngine(ctx).ID(fileID).Get(pf) + if err != nil { + return nil, err + } + if !has { + return nil, ErrPackageFileNotExist + } + return pf, nil +} + +// GetFileForVersionByName gets a file of a version by name +func GetFileForVersionByName(ctx context.Context, versionID int64, name, key string) (*PackageFile, error) { + if name == "" { + return nil, ErrPackageFileNotExist + } + + pf := &PackageFile{} + + has, err := db.GetEngine(ctx).Where(builder.Eq{ + "version_id": versionID, + "lower_name": strings.ToLower(name), + "composite_key": key, + }).Get(pf) + if err != nil { + return nil, err + } + if !has { + return nil, ErrPackageFileNotExist + } + return pf, nil +} + +// DeleteFileByID deletes a file +func DeleteFileByID(ctx context.Context, fileID int64) error { + _, err := db.GetEngine(ctx).ID(fileID).Delete(&PackageFile{}) + return err +} + +// PackageFileSearchOptions are options for SearchXXX methods +type PackageFileSearchOptions struct { + OwnerID int64 + PackageType Type + VersionID int64 + Query string + CompositeKey string + Properties map[string]string + OlderThan time.Duration + HashAlgorithm string + Hash string + db.Paginator +} + +func (opts *PackageFileSearchOptions) toConds() builder.Cond { + cond := builder.NewCond() + + if opts.VersionID != 0 { + cond = cond.And(builder.Eq{"package_file.version_id": opts.VersionID}) + } else if opts.OwnerID != 0 || (opts.PackageType != "" && opts.PackageType != "all") { + var versionCond builder.Cond = builder.Eq{ + "package_version.is_internal": false, + } + if opts.OwnerID != 0 { + versionCond = versionCond.And(builder.Eq{"package.owner_id": opts.OwnerID}) + } + if opts.PackageType != "" && opts.PackageType != "all" { + versionCond = versionCond.And(builder.Eq{"package.type": opts.PackageType}) + } + + in := builder. + Select("package_version.id"). + From("package_version"). + InnerJoin("package", "package.id = package_version.package_id"). + Where(versionCond) + + cond = cond.And(builder.In("package_file.version_id", in)) + } + if opts.CompositeKey != "" { + cond = cond.And(builder.Eq{"package_file.composite_key": opts.CompositeKey}) + } + if opts.Query != "" { + cond = cond.And(builder.Like{"package_file.lower_name", strings.ToLower(opts.Query)}) + } + + if len(opts.Properties) != 0 { + var propsCond builder.Cond = builder.Eq{ + "package_property.ref_type": PropertyTypeFile, + } + propsCond = propsCond.And(builder.Expr("package_property.ref_id = package_file.id")) + + propsCondBlock := builder.NewCond() + for name, value := range opts.Properties { + propsCondBlock = propsCondBlock.Or(builder.Eq{ + "package_property.name": name, + "package_property.value": value, + }) + } + propsCond = propsCond.And(propsCondBlock) + + cond = cond.And(builder.Eq{ + strconv.Itoa(len(opts.Properties)): builder.Select("COUNT(*)").Where(propsCond).From("package_property"), + }) + } + + if opts.OlderThan != 0 { + cond = cond.And(builder.Lt{"package_file.created_unix": time.Now().Add(-opts.OlderThan).Unix()}) + } + + if opts.Hash != "" { + var field string + switch strings.ToLower(opts.HashAlgorithm) { + case "md5": + field = "package_blob.hash_md5" + case "sha1": + field = "package_blob.hash_sha1" + case "sha256": + field = "package_blob.hash_sha256" + case "sha512": + fallthrough + default: // default to SHA512 if not specified or unknown + field = "package_blob.hash_sha512" + } + innerCond := builder. + Expr("package_blob.id = package_file.blob_id"). + And(builder.Eq{field: opts.Hash}) + cond = cond.And(builder.Exists(builder.Select("package_blob.id").From("package_blob").Where(innerCond))) + } + + return cond +} + +// SearchFiles gets all files of packages matching the search options +func SearchFiles(ctx context.Context, opts *PackageFileSearchOptions) ([]*PackageFile, int64, error) { + sess := db.GetEngine(ctx). + Where(opts.toConds()) + + if opts.Paginator != nil { + sess = db.SetSessionPagination(sess, opts) + } + + pfs := make([]*PackageFile, 0, 10) + count, err := sess.FindAndCount(&pfs) + return pfs, count, err +} + +// CalculateFileSize sums up all blob sizes matching the search options. +// It does NOT respect the deduplication of blobs. +func CalculateFileSize(ctx context.Context, opts *PackageFileSearchOptions) (int64, error) { + return db.GetEngine(ctx). + Table("package_file"). + Where(opts.toConds()). + Join("INNER", "package_blob", "package_blob.id = package_file.blob_id"). + SumInt(new(PackageBlob), "size") +} diff --git a/models/packages/package_property.go b/models/packages/package_property.go new file mode 100644 index 0000000..e017001 --- /dev/null +++ b/models/packages/package_property.go @@ -0,0 +1,121 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package packages + +import ( + "context" + + "code.gitea.io/gitea/models/db" + + "xorm.io/builder" +) + +func init() { + db.RegisterModel(new(PackageProperty)) +} + +type PropertyType int64 + +const ( + // PropertyTypeVersion means the reference is a package version + 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 +type PackageProperty struct { + ID int64 `xorm:"pk autoincr"` + RefType PropertyType `xorm:"INDEX NOT NULL"` + RefID int64 `xorm:"INDEX NOT NULL"` + Name string `xorm:"INDEX NOT NULL"` + Value string `xorm:"TEXT NOT NULL"` +} + +// InsertProperty creates a property +func InsertProperty(ctx context.Context, refType PropertyType, refID int64, name, value string) (*PackageProperty, error) { + pp := &PackageProperty{ + RefType: refType, + RefID: refID, + Name: name, + Value: value, + } + + _, err := db.GetEngine(ctx).Insert(pp) + return pp, err +} + +// GetProperties gets all properties +func GetProperties(ctx context.Context, refType PropertyType, refID int64) ([]*PackageProperty, error) { + pps := make([]*PackageProperty, 0, 10) + return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ?", refType, refID).Find(&pps) +} + +// GetPropertiesByName gets all properties with a specific name +func GetPropertiesByName(ctx context.Context, refType PropertyType, refID int64, name string) ([]*PackageProperty, error) { + pps := make([]*PackageProperty, 0, 10) + return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).Find(&pps) +} + +// UpdateProperty updates a property +func UpdateProperty(ctx context.Context, pp *PackageProperty) error { + _, err := db.GetEngine(ctx).ID(pp.ID).Update(pp) + return err +} + +// DeleteAllProperties deletes all properties of a ref +func DeleteAllProperties(ctx context.Context, refType PropertyType, refID int64) error { + _, err := db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ?", refType, refID).Delete(&PackageProperty{}) + return err +} + +// DeletePropertyByID deletes a property +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 +} + +type DistinctPropertyDependency struct { + Name string + Value string +} + +// GetDistinctPropertyValues returns all distinct property values for a given type. +// Optional: Search only in dependence of another property. +func GetDistinctPropertyValues(ctx context.Context, packageType Type, ownerID int64, refType PropertyType, propertyName string, dep *DistinctPropertyDependency) ([]string, error) { + var cond builder.Cond = builder.Eq{ + "package_property.ref_type": refType, + "package_property.name": propertyName, + "package.type": packageType, + "package.owner_id": ownerID, + } + if dep != nil { + innerCond := builder. + Expr("pp.ref_id = package_property.ref_id"). + And(builder.Eq{ + "pp.ref_type": refType, + "pp.name": dep.Name, + "pp.value": dep.Value, + }) + cond = cond.And(builder.Exists(builder.Select("pp.ref_id").From("package_property pp").Where(innerCond))) + } + + values := make([]string, 0, 5) + return values, db.GetEngine(ctx). + Table("package_property"). + Distinct("package_property.value"). + Join("INNER", "package_file", "package_file.id = package_property.ref_id"). + Join("INNER", "package_version", "package_version.id = package_file.version_id"). + Join("INNER", "package", "package.id = package_version.package_id"). + Where(cond). + Find(&values) +} diff --git a/models/packages/package_test.go b/models/packages/package_test.go new file mode 100644 index 0000000..1c96e08 --- /dev/null +++ b/models/packages/package_test.go @@ -0,0 +1,319 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package packages_test + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + packages_model "code.gitea.io/gitea/models/packages" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + _ "code.gitea.io/gitea/models" + _ "code.gitea.io/gitea/models/actions" + _ "code.gitea.io/gitea/models/activities" + + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m) +} + +func prepareExamplePackage(t *testing.T) *packages_model.Package { + require.NoError(t, unittest.PrepareTestDatabase()) + + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) + + p0 := &packages_model.Package{ + OwnerID: owner.ID, + RepoID: repo.ID, + LowerName: "package", + Type: packages_model.TypeGeneric, + } + + p, err := packages_model.TryInsertPackage(db.DefaultContext, p0) + require.NotNil(t, p) + require.NoError(t, err) + require.Equal(t, *p0, *p) + return p +} + +func deletePackage(t *testing.T, p *packages_model.Package) { + err := packages_model.DeletePackageByID(db.DefaultContext, p.ID) + require.NoError(t, err) +} + +func TestTryInsertPackage(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + p0 := &packages_model.Package{ + OwnerID: owner.ID, + LowerName: "package", + } + + // Insert package should return the package and yield no error + p, err := packages_model.TryInsertPackage(db.DefaultContext, p0) + require.NotNil(t, p) + require.NoError(t, err) + require.Equal(t, *p0, *p) + + // Insert same package again should return the same package and yield ErrDuplicatePackage + p, err = packages_model.TryInsertPackage(db.DefaultContext, p0) + require.NotNil(t, p) + require.IsType(t, packages_model.ErrDuplicatePackage, err) + require.Equal(t, *p0, *p) + + err = packages_model.DeletePackageByID(db.DefaultContext, p0.ID) + require.NoError(t, err) +} + +func TestGetPackageByID(t *testing.T) { + p0 := prepareExamplePackage(t) + + // Get package should return package and yield no error + p, err := packages_model.GetPackageByID(db.DefaultContext, p0.ID) + require.NotNil(t, p) + require.Equal(t, *p0, *p) + require.NoError(t, err) + + // Get package with non-existng ID should yield ErrPackageNotExist + p, err = packages_model.GetPackageByID(db.DefaultContext, 999) + require.Nil(t, p) + require.Error(t, err) + require.IsType(t, packages_model.ErrPackageNotExist, err) + + deletePackage(t, p0) +} + +func TestDeletePackageByID(t *testing.T) { + p0 := prepareExamplePackage(t) + + // Delete existing package should yield no error + err := packages_model.DeletePackageByID(db.DefaultContext, p0.ID) + require.NoError(t, err) + + // Delete (now) non-existing package should yield ErrPackageNotExist + err = packages_model.DeletePackageByID(db.DefaultContext, p0.ID) + require.Error(t, err) + require.IsType(t, packages_model.ErrPackageNotExist, err) +} + +func TestSetRepositoryLink(t *testing.T) { + p0 := prepareExamplePackage(t) + + // Set repository link to package should yield no error and package RepoID should be updated + err := packages_model.SetRepositoryLink(db.DefaultContext, p0.ID, 5) + require.NoError(t, err) + + p, err := packages_model.GetPackageByID(db.DefaultContext, p0.ID) + require.NoError(t, err) + require.EqualValues(t, 5, p.RepoID) + + // Set repository link to non-existing package should yied ErrPackageNotExist + err = packages_model.SetRepositoryLink(db.DefaultContext, 999, 5) + require.Error(t, err) + require.IsType(t, packages_model.ErrPackageNotExist, err) + + deletePackage(t, p0) +} + +func TestUnlinkRepositoryFromAllPackages(t *testing.T) { + p0 := prepareExamplePackage(t) + + // Unlink repository from all packages should yield no error and package with p0.ID should have RepoID 0 + err := packages_model.UnlinkRepositoryFromAllPackages(db.DefaultContext, p0.RepoID) + require.NoError(t, err) + + p, err := packages_model.GetPackageByID(db.DefaultContext, p0.ID) + require.NoError(t, err) + require.EqualValues(t, 0, p.RepoID) + + // Unlink repository again from all packages should also yield no error + err = packages_model.UnlinkRepositoryFromAllPackages(db.DefaultContext, p0.RepoID) + require.NoError(t, err) + + deletePackage(t, p0) +} + +func TestGetPackageByName(t *testing.T) { + p0 := prepareExamplePackage(t) + + // Get package should return package and yield no error + p, err := packages_model.GetPackageByName(db.DefaultContext, p0.OwnerID, p0.Type, p0.LowerName) + require.NotNil(t, p) + require.Equal(t, *p0, *p) + require.NoError(t, err) + + // Get package with uppercase name should return package and yield no error + p, err = packages_model.GetPackageByName(db.DefaultContext, p0.OwnerID, p0.Type, "Package") + require.NotNil(t, p) + require.Equal(t, *p0, *p) + require.NoError(t, err) + + // Get package with wrong owner ID, type or name should return no package and yield ErrPackageNotExist + p, err = packages_model.GetPackageByName(db.DefaultContext, 999, p0.Type, p0.LowerName) + require.Nil(t, p) + require.Error(t, err) + require.IsType(t, packages_model.ErrPackageNotExist, err) + p, err = packages_model.GetPackageByName(db.DefaultContext, p0.OwnerID, packages_model.TypeDebian, p0.LowerName) + require.Nil(t, p) + require.Error(t, err) + require.IsType(t, packages_model.ErrPackageNotExist, err) + p, err = packages_model.GetPackageByName(db.DefaultContext, p0.OwnerID, p0.Type, "package1") + require.Nil(t, p) + require.Error(t, err) + require.IsType(t, packages_model.ErrPackageNotExist, err) + + deletePackage(t, p0) +} + +func TestHasCountPackages(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) + + p, err := packages_model.TryInsertPackage(db.DefaultContext, &packages_model.Package{ + OwnerID: owner.ID, + RepoID: repo.ID, + LowerName: "package", + }) + require.NotNil(t, p) + require.NoError(t, err) + + // A package without package versions gets automatically cleaned up and should return false for owner + has, err := packages_model.HasOwnerPackages(db.DefaultContext, owner.ID) + require.False(t, has) + require.NoError(t, err) + count, err := packages_model.CountOwnerPackages(db.DefaultContext, owner.ID) + require.EqualValues(t, 0, count) + require.NoError(t, err) + + // A package without package versions gets automatically cleaned up and should return false for repository + has, err = packages_model.HasRepositoryPackages(db.DefaultContext, repo.ID) + require.False(t, has) + require.NoError(t, err) + count, err = packages_model.CountRepositoryPackages(db.DefaultContext, repo.ID) + require.EqualValues(t, 0, count) + require.NoError(t, err) + + pv, err := packages_model.GetOrInsertVersion(db.DefaultContext, &packages_model.PackageVersion{ + PackageID: p.ID, + LowerVersion: "internal", + IsInternal: true, + }) + require.NotNil(t, pv) + require.NoError(t, err) + + // A package with an internal package version gets automatically cleaned up and should return false + has, err = packages_model.HasOwnerPackages(db.DefaultContext, owner.ID) + require.False(t, has) + require.NoError(t, err) + count, err = packages_model.CountOwnerPackages(db.DefaultContext, owner.ID) + require.EqualValues(t, 0, count) + require.NoError(t, err) + has, err = packages_model.HasRepositoryPackages(db.DefaultContext, repo.ID) + require.False(t, has) + require.NoError(t, err) + count, err = packages_model.CountRepositoryPackages(db.DefaultContext, repo.ID) + require.EqualValues(t, 0, count) + require.NoError(t, err) + + pv, err = packages_model.GetOrInsertVersion(db.DefaultContext, &packages_model.PackageVersion{ + PackageID: p.ID, + LowerVersion: "normal", + IsInternal: false, + }) + require.NotNil(t, pv) + require.NoError(t, err) + + // A package with a normal package version should return true + has, err = packages_model.HasOwnerPackages(db.DefaultContext, owner.ID) + require.True(t, has) + require.NoError(t, err) + count, err = packages_model.CountOwnerPackages(db.DefaultContext, owner.ID) + require.EqualValues(t, 1, count) + require.NoError(t, err) + has, err = packages_model.HasRepositoryPackages(db.DefaultContext, repo.ID) + require.True(t, has) + require.NoError(t, err) + count, err = packages_model.CountRepositoryPackages(db.DefaultContext, repo.ID) + require.EqualValues(t, 1, count) + require.NoError(t, err) + + pv2, err := packages_model.GetOrInsertVersion(db.DefaultContext, &packages_model.PackageVersion{ + PackageID: p.ID, + LowerVersion: "normal2", + IsInternal: false, + }) + require.NotNil(t, pv2) + require.NoError(t, err) + + // A package withmultiple package versions should be counted only once + has, err = packages_model.HasOwnerPackages(db.DefaultContext, owner.ID) + require.True(t, has) + require.NoError(t, err) + count, err = packages_model.CountOwnerPackages(db.DefaultContext, owner.ID) + require.EqualValues(t, 1, count) + require.NoError(t, err) + has, err = packages_model.HasRepositoryPackages(db.DefaultContext, repo.ID) + require.True(t, has) + require.NoError(t, err) + count, err = packages_model.CountRepositoryPackages(db.DefaultContext, repo.ID) + require.EqualValues(t, 1, count) + require.NoError(t, err) + + // For owner ID 0 there should be no packages + has, err = packages_model.HasOwnerPackages(db.DefaultContext, 0) + require.False(t, has) + require.NoError(t, err) + count, err = packages_model.CountOwnerPackages(db.DefaultContext, 0) + require.EqualValues(t, 0, count) + require.NoError(t, err) + + // For repo ID 0 there should be no packages + has, err = packages_model.HasRepositoryPackages(db.DefaultContext, 0) + require.False(t, has) + require.NoError(t, err) + count, err = packages_model.CountRepositoryPackages(db.DefaultContext, 0) + require.EqualValues(t, 0, count) + require.NoError(t, err) + + p1, err := packages_model.TryInsertPackage(db.DefaultContext, &packages_model.Package{ + OwnerID: owner.ID, + LowerName: "package0", + }) + require.NotNil(t, p1) + require.NoError(t, err) + p1v, err := packages_model.GetOrInsertVersion(db.DefaultContext, &packages_model.PackageVersion{ + PackageID: p1.ID, + LowerVersion: "normal", + IsInternal: false, + }) + require.NotNil(t, p1v) + require.NoError(t, err) + + // Owner owner.ID should have two packages now + has, err = packages_model.HasOwnerPackages(db.DefaultContext, owner.ID) + require.True(t, has) + require.NoError(t, err) + count, err = packages_model.CountOwnerPackages(db.DefaultContext, owner.ID) + require.EqualValues(t, 2, count) + require.NoError(t, err) + + // For repo ID 0 there should be now one package, because p1 is not assigned to a repo + has, err = packages_model.HasRepositoryPackages(db.DefaultContext, 0) + require.True(t, has) + require.NoError(t, err) + count, err = packages_model.CountRepositoryPackages(db.DefaultContext, 0) + require.EqualValues(t, 1, count) + require.NoError(t, err) +} diff --git a/models/packages/package_version.go b/models/packages/package_version.go new file mode 100644 index 0000000..278e8e3 --- /dev/null +++ b/models/packages/package_version.go @@ -0,0 +1,348 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package packages + +import ( + "context" + "strconv" + "strings" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +// ErrDuplicatePackageVersion indicates a duplicated package version error +var ErrDuplicatePackageVersion = util.NewAlreadyExistErrorf("package version already exists") + +func init() { + db.RegisterModel(new(PackageVersion)) +} + +// PackageVersion represents a package version +type PackageVersion struct { + ID int64 `xorm:"pk autoincr"` + PackageID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + CreatorID int64 `xorm:"NOT NULL DEFAULT 0"` + Version string `xorm:"NOT NULL"` + LowerVersion string `xorm:"UNIQUE(s) INDEX NOT NULL"` + CreatedUnix timeutil.TimeStamp `xorm:"created INDEX NOT NULL"` + IsInternal bool `xorm:"INDEX NOT NULL DEFAULT false"` + MetadataJSON string `xorm:"metadata_json LONGTEXT"` + DownloadCount int64 `xorm:"NOT NULL DEFAULT 0"` +} + +// GetOrInsertVersion inserts a version. If the same version exist already ErrDuplicatePackageVersion is returned +func GetOrInsertVersion(ctx context.Context, pv *PackageVersion) (*PackageVersion, error) { + e := db.GetEngine(ctx) + + existing := &PackageVersion{} + + has, err := e.Where(builder.Eq{ + "package_id": pv.PackageID, + "lower_version": pv.LowerVersion, + }).Get(existing) + if err != nil { + return nil, err + } + if has { + return existing, ErrDuplicatePackageVersion + } + if _, err = e.Insert(pv); err != nil { + return nil, err + } + return pv, nil +} + +// UpdateVersion updates a version +func UpdateVersion(ctx context.Context, pv *PackageVersion) error { + _, err := db.GetEngine(ctx).ID(pv.ID).Update(pv) + return err +} + +// IncrementDownloadCounter increments the download counter of a version +func IncrementDownloadCounter(ctx context.Context, versionID int64) error { + _, err := db.GetEngine(ctx).Exec("UPDATE `package_version` SET `download_count` = `download_count` + 1 WHERE `id` = ?", versionID) + return err +} + +// GetVersionByID gets a version by id +func GetVersionByID(ctx context.Context, versionID int64) (*PackageVersion, error) { + pv := &PackageVersion{} + + has, err := db.GetEngine(ctx).ID(versionID).Get(pv) + if err != nil { + return nil, err + } + if !has { + return nil, ErrPackageNotExist + } + return pv, nil +} + +// GetVersionByNameAndVersion gets a version by name and version number +func GetVersionByNameAndVersion(ctx context.Context, ownerID int64, packageType Type, name, version string) (*PackageVersion, error) { + return getVersionByNameAndVersion(ctx, ownerID, packageType, name, version, false) +} + +// GetInternalVersionByNameAndVersion gets a version by name and version number +func GetInternalVersionByNameAndVersion(ctx context.Context, ownerID int64, packageType Type, name, version string) (*PackageVersion, error) { + return getVersionByNameAndVersion(ctx, ownerID, packageType, name, version, true) +} + +func getVersionByNameAndVersion(ctx context.Context, ownerID int64, packageType Type, name, version string, isInternal bool) (*PackageVersion, error) { + pvs, _, err := SearchVersions(ctx, &PackageSearchOptions{ + OwnerID: ownerID, + Type: packageType, + Name: SearchValue{ + ExactMatch: true, + Value: name, + }, + Version: SearchValue{ + ExactMatch: true, + Value: version, + }, + IsInternal: optional.Some(isInternal), + Paginator: db.NewAbsoluteListOptions(0, 1), + }) + if err != nil { + return nil, err + } + if len(pvs) == 0 { + return nil, ErrPackageNotExist + } + return pvs[0], nil +} + +// GetVersionsByPackageType gets all versions of a specific type +func GetVersionsByPackageType(ctx context.Context, ownerID int64, packageType Type) ([]*PackageVersion, error) { + pvs, _, err := SearchVersions(ctx, &PackageSearchOptions{ + OwnerID: ownerID, + Type: packageType, + IsInternal: optional.Some(false), + }) + return pvs, err +} + +// GetVersionsByPackageName gets all versions of a specific package +func GetVersionsByPackageName(ctx context.Context, ownerID int64, packageType Type, name string) ([]*PackageVersion, error) { + pvs, _, err := SearchVersions(ctx, &PackageSearchOptions{ + OwnerID: ownerID, + Type: packageType, + Name: SearchValue{ + ExactMatch: true, + Value: name, + }, + IsInternal: optional.Some(false), + }) + return pvs, err +} + +// DeleteVersionByID deletes a version by id +func DeleteVersionByID(ctx context.Context, versionID int64) error { + _, err := db.GetEngine(ctx).ID(versionID).Delete(&PackageVersion{}) + return err +} + +// HasVersionFileReferences checks if there are associated files +func HasVersionFileReferences(ctx context.Context, versionID int64) (bool, error) { + return db.GetEngine(ctx).Get(&PackageFile{ + VersionID: versionID, + }) +} + +// SearchValue describes a value to search +// If ExactMatch is true, the field must match the value otherwise a LIKE search is performed. +type SearchValue struct { + Value string + ExactMatch bool +} + +type VersionSort = string + +const ( + SortNameAsc VersionSort = "name_asc" + SortNameDesc VersionSort = "name_desc" + SortVersionAsc VersionSort = "version_asc" + SortVersionDesc VersionSort = "version_desc" + SortCreatedAsc VersionSort = "created_asc" + SortCreatedDesc VersionSort = "created_desc" +) + +// PackageSearchOptions are options for SearchXXX methods +// All fields optional and are not used if they have their default value (nil, "", 0) +type PackageSearchOptions struct { + OwnerID int64 + RepoID int64 + Type Type + PackageID int64 + Name SearchValue // only results with the specific name are found + Version SearchValue // only results with the specific version are found + Properties map[string]string // only results are found which contain all listed version properties with the specific value + IsInternal optional.Option[bool] + HasFileWithName string // only results are found which are associated with a file with the specific name + HasFiles optional.Option[bool] // only results are found which have associated files + Sort VersionSort + db.Paginator +} + +func (opts *PackageSearchOptions) ToConds() builder.Cond { + cond := builder.NewCond() + if opts.IsInternal.Has() { + cond = builder.Eq{ + "package_version.is_internal": opts.IsInternal.Value(), + } + } + + if opts.OwnerID != 0 { + cond = cond.And(builder.Eq{"package.owner_id": opts.OwnerID}) + } + if opts.RepoID != 0 { + cond = cond.And(builder.Eq{"package.repo_id": opts.RepoID}) + } + if opts.Type != "" && opts.Type != "all" { + cond = cond.And(builder.Eq{"package.type": opts.Type}) + } + if opts.PackageID != 0 { + cond = cond.And(builder.Eq{"package.id": opts.PackageID}) + } + if opts.Name.Value != "" { + if opts.Name.ExactMatch { + cond = cond.And(builder.Eq{"package.lower_name": strings.ToLower(opts.Name.Value)}) + } else { + cond = cond.And(builder.Like{"package.lower_name", strings.ToLower(opts.Name.Value)}) + } + } + if opts.Version.Value != "" { + if opts.Version.ExactMatch { + cond = cond.And(builder.Eq{"package_version.lower_version": strings.ToLower(opts.Version.Value)}) + } else { + cond = cond.And(builder.Like{"package_version.lower_version", strings.ToLower(opts.Version.Value)}) + } + } + + if len(opts.Properties) != 0 { + var propsCond builder.Cond = builder.Eq{ + "package_property.ref_type": PropertyTypeVersion, + } + propsCond = propsCond.And(builder.Expr("package_property.ref_id = package_version.id")) + + propsCondBlock := builder.NewCond() + for name, value := range opts.Properties { + propsCondBlock = propsCondBlock.Or(builder.Eq{ + "package_property.name": name, + "package_property.value": value, + }) + } + propsCond = propsCond.And(propsCondBlock) + + cond = cond.And(builder.Eq{ + strconv.Itoa(len(opts.Properties)): builder.Select("COUNT(*)").Where(propsCond).From("package_property"), + }) + } + + if opts.HasFileWithName != "" { + fileCond := builder.Expr("package_file.version_id = package_version.id").And(builder.Eq{"package_file.lower_name": strings.ToLower(opts.HasFileWithName)}) + + cond = cond.And(builder.Exists(builder.Select("package_file.id").From("package_file").Where(fileCond))) + } + + if opts.HasFiles.Has() { + filesCond := builder.Exists(builder.Select("package_file.id").From("package_file").Where(builder.Expr("package_file.version_id = package_version.id"))) + + if !opts.HasFiles.Value() { + filesCond = builder.Not{filesCond} + } + + cond = cond.And(filesCond) + } + + return cond +} + +func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) { + switch opts.Sort { + case SortNameAsc: + e.Asc("package.name") + case SortNameDesc: + e.Desc("package.name") + case SortVersionDesc: + e.Desc("package_version.version") + case SortVersionAsc: + e.Asc("package_version.version") + case SortCreatedAsc: + e.Asc("package_version.created_unix") + default: + e.Desc("package_version.created_unix") + } + + // Sort by id for stable order with duplicates in the other field + e.Asc("package_version.id") +} + +// SearchVersions gets all versions of packages matching the search options +func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) { + sess := db.GetEngine(ctx). + Select("package_version.*"). + Table("package_version"). + Join("INNER", "package", "package.id = package_version.package_id"). + Where(opts.ToConds()) + + opts.configureOrderBy(sess) + + if opts.Paginator != nil { + sess = db.SetSessionPagination(sess, opts) + } + + pvs := make([]*PackageVersion, 0, 10) + count, err := sess.FindAndCount(&pvs) + return pvs, count, err +} + +// SearchLatestVersions gets the latest version of every package matching the search options +func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) { + in := builder. + Select("MAX(package_version.id)"). + From("package_version"). + InnerJoin("package", "package.id = package_version.package_id"). + Where(opts.ToConds()). + GroupBy("package_version.package_id") + + sess := db.GetEngine(ctx). + Select("package_version.*"). + Table("package_version"). + Join("INNER", "package", "package.id = package_version.package_id"). + Where(builder.In("package_version.id", in)) + + opts.configureOrderBy(sess) + + if opts.Paginator != nil { + sess = db.SetSessionPagination(sess, opts) + } + + pvs := make([]*PackageVersion, 0, 10) + count, err := sess.FindAndCount(&pvs) + return pvs, count, err +} + +// ExistVersion checks if a version matching the search options exist +func ExistVersion(ctx context.Context, opts *PackageSearchOptions) (bool, error) { + return db.GetEngine(ctx). + Where(opts.ToConds()). + Table("package_version"). + Join("INNER", "package", "package.id = package_version.package_id"). + Exist(new(PackageVersion)) +} + +// CountVersions counts all versions of packages matching the search options +func CountVersions(ctx context.Context, opts *PackageSearchOptions) (int64, error) { + return db.GetEngine(ctx). + Where(opts.ToConds()). + Table("package_version"). + Join("INNER", "package", "package.id = package_version.package_id"). + Count(new(PackageVersion)) +} diff --git a/models/packages/rpm/search.go b/models/packages/rpm/search.go new file mode 100644 index 0000000..e697421 --- /dev/null +++ b/models/packages/rpm/search.go @@ -0,0 +1,23 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package rpm + +import ( + "context" + + packages_model "code.gitea.io/gitea/models/packages" + rpm_module "code.gitea.io/gitea/modules/packages/rpm" +) + +// GetGroups gets all available groups +func GetGroups(ctx context.Context, ownerID int64) ([]string, error) { + return packages_model.GetDistinctPropertyValues( + ctx, + packages_model.TypeRpm, + ownerID, + packages_model.PropertyTypeFile, + rpm_module.PropertyGroup, + nil, + ) +} |