summaryrefslogtreecommitdiffstats
path: root/models/packages
diff options
context:
space:
mode:
Diffstat (limited to 'models/packages')
-rw-r--r--models/packages/alpine/search.go53
-rw-r--r--models/packages/conan/references.go170
-rw-r--r--models/packages/conan/search.go149
-rw-r--r--models/packages/conda/search.go63
-rw-r--r--models/packages/container/const.go9
-rw-r--r--models/packages/container/search.go285
-rw-r--r--models/packages/cran/search.go90
-rw-r--r--models/packages/debian/search.go157
-rw-r--r--models/packages/debian/search_test.go93
-rw-r--r--models/packages/descriptor.go260
-rw-r--r--models/packages/nuget/search.go70
-rw-r--r--models/packages/package.go351
-rw-r--r--models/packages/package_blob.go154
-rw-r--r--models/packages/package_blob_upload.go79
-rw-r--r--models/packages/package_cleanup_rule.go109
-rw-r--r--models/packages/package_file.go232
-rw-r--r--models/packages/package_property.go121
-rw-r--r--models/packages/package_test.go319
-rw-r--r--models/packages/package_version.go348
-rw-r--r--models/packages/rpm/search.go23
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,
+ )
+}