aboutsummaryrefslogtreecommitdiffstats
path: root/models/packages
diff options
context:
space:
mode:
authorKN4CK3R <admin@oldschoolhack.me>2022-03-30 10:42:47 +0200
committerGitHub <noreply@github.com>2022-03-30 16:42:47 +0800
commit1d332342db6d5bd4e1552d8d46720bf1b948c26b (patch)
treeca0c8931e5da85e71037ed43d7a90826ba708d9d /models/packages
parent2bce1ea9862c70ebb69963e65bb84dcad6ebb31c (diff)
downloadgitea-1d332342db6d5bd4e1552d8d46720bf1b948c26b.tar.gz
gitea-1d332342db6d5bd4e1552d8d46720bf1b948c26b.zip
Add Package Registry (#16510)
* Added package store settings. * Added models. * Added generic package registry. * Added tests. * Added NuGet package registry. * Moved service index to api file. * Added NPM package registry. * Added Maven package registry. * Added PyPI package registry. * Summary is deprecated. * Changed npm name. * Sanitize project url. * Allow only scoped packages. * Added user interface. * Changed method name. * Added missing migration file. * Set page info. * Added documentation. * Added documentation links. * Fixed wrong error message. * Lint template files. * Fixed merge errors. * Fixed unit test storage path. * Switch to json module. * Added suggestions. * Added package webhook. * Add package api. * Fixed swagger file. * Fixed enum and comments. * Fixed NuGet pagination. * Print test names. * Added api tests. * Fixed access level. * Fix User unmarshal. * Added RubyGems package registry. * Fix lint. * Implemented io.Writer. * Added support for sha256/sha512 checksum files. * Improved maven-metadata.xml support. * Added support for symbol package uploads. * Added tests. * Added overview docs. * Added npm dependencies and keywords. * Added no-packages information. * Display file size. * Display asset count. * Fixed filter alignment. * Added package icons. * Formatted instructions. * Allow anonymous package downloads. * Fixed comments. * Fixed postgres test. * Moved file. * Moved models to models/packages. * Use correct error response format per client. * Use simpler search form. * Fixed IsProd. * Restructured data model. * Prevent empty filename. * Fix swagger. * Implemented user/org registry. * Implemented UI. * Use GetUserByIDCtx. * Use table for dependencies. * make svg * Added support for unscoped npm packages. * Add support for npm dist tags. * Added tests for npm tags. * Unlink packages if repository gets deleted. * Prevent user/org delete if a packages exist. * Use package unlink in repository service. * Added support for composer packages. * Restructured package docs. * Added missing tests. * Fixed generic content page. * Fixed docs. * Fixed swagger. * Added missing type. * Fixed ambiguous column. * Organize content store by sha256 hash. * Added admin package management. * Added support for sorting. * Add support for multiple identical versions/files. * Added missing repository unlink. * Added file properties. * make fmt * lint * Added Conan package registry. * Updated docs. * Unify package names. * Added swagger enum. * Use longer TEXT column type. * Removed version composite key. * Merged package and container registry. * Removed index. * Use dedicated package router. * Moved files to new location. * Updated docs. * Fixed JOIN order. * Fixed GROUP BY statement. * Fixed GROUP BY #2. * Added symbol server support. * Added more tests. * Set NOT NULL. * Added setting to disable package registries. * Moved auth into service. * refactor * Use ctx everywhere. * Added package cleanup task. * Changed packages path. * Added container registry. * Refactoring * Updated comparison. * Fix swagger. * Fixed table order. * Use token auth for npm routes. * Enabled ReverseProxy auth. * Added packages link for orgs. * Fixed anonymous org access. * Enable copy button for setup instructions. * Merge error * Added suggestions. * Fixed merge. * Handle "generic". * Added link for TODO. * Added suggestions. * Changed temporary buffer filename. * Added suggestions. * Apply suggestions from code review Co-authored-by: Thomas Boerger <thomas@webhippie.de> * Update docs/content/doc/packages/nuget.en-us.md Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Thomas Boerger <thomas@webhippie.de>
Diffstat (limited to 'models/packages')
-rw-r--r--models/packages/conan/references.go171
-rw-r--r--models/packages/conan/search.go149
-rw-r--r--models/packages/container/const.go10
-rw-r--r--models/packages/container/search.go227
-rw-r--r--models/packages/descriptor.go192
-rw-r--r--models/packages/package.go213
-rw-r--r--models/packages/package_blob.go85
-rw-r--r--models/packages/package_blob_upload.go81
-rw-r--r--models/packages/package_file.go201
-rw-r--r--models/packages/package_property.go70
-rw-r--r--models/packages/package_version.go316
11 files changed, 1715 insertions, 0 deletions
diff --git a/models/packages/conan/references.go b/models/packages/conan/references.go
new file mode 100644
index 0000000000..4b7b201430
--- /dev/null
+++ b/models/packages/conan/references.go
@@ -0,0 +1,171 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package conan
+
+import (
+ "context"
+ "errors"
+ "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"
+
+ "xorm.io/builder"
+)
+
+var (
+ ErrRecipeReferenceNotExist = errors.New("Recipe reference does not exist")
+ ErrPackageReferenceNotExist = errors.New("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").
+ Join("INNER", "package_version", "package_version.id = package_file.version_id").
+ Join("INNER", "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").
+ Join("INNER", "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 0000000000..c274a7ce02
--- /dev/null
+++ b/models/packages/conan/search.go
@@ -0,0 +1,149 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package conan
+
+import (
+ "context"
+ "fmt"
+ "strconv"
+ "strings"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/packages"
+ 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").
+ Join("INNER", "package_version", "package_version.id = package_file.version_id").
+ Join("INNER", "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(map[string]bool)
+ 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[recipe] = true
+ }
+
+ 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/container/const.go b/models/packages/container/const.go
new file mode 100644
index 0000000000..9d3ed64a6e
--- /dev/null
+++ b/models/packages/container/const.go
@@ -0,0 +1,10 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package 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 0000000000..972cac9528
--- /dev/null
+++ b/models/packages/container/search.go
@@ -0,0 +1,227 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package container
+
+import (
+ "context"
+ "errors"
+ "strings"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/packages"
+ container_module "code.gitea.io/gitea/modules/packages/container"
+
+ "xorm.io/builder"
+)
+
+var ErrContainerBlobNotExist = errors.New("Container blob does not exist")
+
+type BlobSearchOptions struct {
+ OwnerID int64
+ Image string
+ Digest string
+ Tag string
+ IsManifest bool
+}
+
+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")))
+ }
+
+ 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
+ }
+
+ pfds := make([]*packages.PackageFileDescriptor, 0, len(pfs))
+ for _, pf := range pfs {
+ pfd, err := packages.GetPackageFileDescriptor(ctx, pf)
+ if err != nil {
+ return nil, err
+ }
+ pfds = append(pfds, pfd)
+ }
+
+ return pfds, nil
+}
+
+// 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
+ 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
+}
+
+// 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()).
+ Desc("package_version.created_unix")
+
+ if opts.Paginator != nil {
+ sess = db.SetSessionPagination(sess, opts)
+ }
+
+ pvs := make([]*packages.PackageVersion, 0, 10)
+ count, err := sess.FindAndCount(&pvs)
+ return pvs, count, err
+}
+
+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)
+}
diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go
new file mode 100644
index 0000000000..3249260f80
--- /dev/null
+++ b/models/packages/descriptor.go
@@ -0,0 +1,192 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package packages
+
+import (
+ "context"
+ "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/composer"
+ "code.gitea.io/gitea/modules/packages/conan"
+ "code.gitea.io/gitea/modules/packages/container"
+ "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/pypi"
+ "code.gitea.io/gitea/modules/packages/rubygems"
+
+ "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
+ Properties PackagePropertyList
+ Metadata interface{}
+ Files []*PackageFileDescriptor
+}
+
+// PackageFileDescriptor describes a package file
+type PackageFileDescriptor struct {
+ File *PackageFile
+ Blob *PackageBlob
+ Properties PackagePropertyList
+}
+
+// PackageWebLink returns the package web link
+func (pd *PackageDescriptor) PackageWebLink() string {
+ return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HTMLURL(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName))
+}
+
+// FullWebLink returns the package version web link
+func (pd *PackageDescriptor) FullWebLink() string {
+ return fmt.Sprintf("%s/%s", pd.PackageWebLink(), 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.GetUserByIDCtx(ctx, p.OwnerID)
+ if err != nil {
+ return nil, err
+ }
+ repository, err := repo_model.GetRepositoryByIDCtx(ctx, p.RepoID)
+ if err != nil && !repo_model.IsErrRepoNotExist(err) {
+ return nil, err
+ }
+ creator, err := user_model.GetUserByIDCtx(ctx, pv.CreatorID)
+ if err != nil {
+ return nil, err
+ }
+ var semVer *version.Version
+ if p.SemverCompatible {
+ semVer, err = version.NewVersion(pv.Version)
+ 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 := make([]*PackageFileDescriptor, 0, len(pfs))
+ for _, pf := range pfs {
+ pfd, err := GetPackageFileDescriptor(ctx, pf)
+ if err != nil {
+ return nil, err
+ }
+ pfds = append(pfds, pfd)
+ }
+
+ var metadata interface{}
+ switch p.Type {
+ case TypeComposer:
+ metadata = &composer.Metadata{}
+ case TypeConan:
+ metadata = &conan.Metadata{}
+ case TypeContainer:
+ metadata = &container.Metadata{}
+ case TypeGeneric:
+ // generic packages have no metadata
+ case TypeNuGet:
+ metadata = &nuget.Metadata{}
+ case TypeNpm:
+ metadata = &npm.Metadata{}
+ case TypeMaven:
+ metadata = &maven.Metadata{}
+ case TypePyPI:
+ metadata = &pypi.Metadata{}
+ case TypeRubyGems:
+ metadata = &rubygems.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,
+ Properties: 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
+}
+
+// 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/package.go b/models/packages/package.go
new file mode 100644
index 0000000000..05170ab3f4
--- /dev/null
+++ b/models/packages/package.go
@@ -0,0 +1,213 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package packages
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+
+ "code.gitea.io/gitea/models/db"
+
+ "xorm.io/builder"
+)
+
+func init() {
+ db.RegisterModel(new(Package))
+}
+
+var (
+ // ErrDuplicatePackage indicates a duplicated package error
+ ErrDuplicatePackage = errors.New("Package does exist already")
+ // ErrPackageNotExist indicates a package not exist error
+ ErrPackageNotExist = errors.New("Package does not exist")
+)
+
+// Type of a package
+type Type string
+
+// List of supported packages
+const (
+ TypeComposer Type = "composer"
+ TypeConan Type = "conan"
+ TypeContainer Type = "container"
+ TypeGeneric Type = "generic"
+ TypeNuGet Type = "nuget"
+ TypeNpm Type = "npm"
+ TypeMaven Type = "maven"
+ TypePyPI Type = "pypi"
+ TypeRubyGems Type = "rubygems"
+)
+
+// Name gets the name of the package type
+func (pt Type) Name() string {
+ switch pt {
+ case TypeComposer:
+ return "Composer"
+ case TypeConan:
+ return "Conan"
+ case TypeContainer:
+ return "Container"
+ case TypeGeneric:
+ return "Generic"
+ case TypeNuGet:
+ return "NuGet"
+ case TypeNpm:
+ return "npm"
+ case TypeMaven:
+ return "Maven"
+ case TypePyPI:
+ return "PyPI"
+ case TypeRubyGems:
+ return "RubyGems"
+ }
+ 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 TypeComposer:
+ return "gitea-composer"
+ case TypeConan:
+ return "gitea-conan"
+ case TypeContainer:
+ return "octicon-container"
+ case TypeGeneric:
+ return "octicon-package"
+ case TypeNuGet:
+ return "gitea-nuget"
+ case TypeNpm:
+ return "gitea-npm"
+ case TypeMaven:
+ return "gitea-maven"
+ case TypePyPI:
+ return "gitea-python"
+ case TypeRubyGems:
+ return "gitea-rubygems"
+ }
+ 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"`
+}
+
+// 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)
+
+ key := &Package{
+ OwnerID: p.OwnerID,
+ Type: p.Type,
+ LowerName: p.LowerName,
+ }
+
+ has, err := e.Get(key)
+ if err != nil {
+ return nil, err
+ }
+ if has {
+ return key, ErrDuplicatePackage
+ }
+ if _, err = e.Insert(p); err != nil {
+ return nil, err
+ }
+ return p, nil
+}
+
+// SetRepositoryLink sets the linked repository
+func SetRepositoryLink(ctx context.Context, packageID, repoID int64) error {
+ _, err := db.GetEngine(ctx).ID(packageID).Cols("repo_id").Update(&Package{RepoID: repoID})
+ 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),
+ }
+
+ 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,
+ }
+
+ ps := make([]*Package, 0, 10)
+ return ps, db.GetEngine(ctx).
+ Where(cond).
+ Find(&ps)
+}
+
+// DeletePackagesIfUnreferenced deletes a package if there are no associated versions
+func DeletePackagesIfUnreferenced(ctx context.Context) error {
+ in := builder.
+ Select("package_version.package_id").
+ From("package").
+ Join("LEFT", "package_version", "package_version.package_id = package.id").
+ Where(builder.Expr("package_version.id IS NULL"))
+
+ _, err := db.GetEngine(ctx).
+ Where(builder.In("package.id", in)).
+ Delete(&Package{})
+
+ return err
+}
+
+// HasOwnerPackages tests if a user/org has packages
+func HasOwnerPackages(ctx context.Context, ownerID int64) (bool, error) {
+ return db.GetEngine(ctx).Where("owner_id = ?", ownerID).Exist(&Package{})
+}
+
+// HasRepositoryPackages tests if a repository has packages
+func HasRepositoryPackages(ctx context.Context, repositoryID int64) (bool, error) {
+ return db.GetEngine(ctx).Where("repo_id = ?", repositoryID).Exist(&Package{})
+}
diff --git a/models/packages/package_blob.go b/models/packages/package_blob.go
new file mode 100644
index 0000000000..d9a8314c88
--- /dev/null
+++ b/models/packages/package_blob.go
@@ -0,0 +1,85 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package packages
+
+import (
+ "context"
+ "errors"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/timeutil"
+)
+
+// ErrPackageBlobNotExist indicates a package blob not exist error
+var ErrPackageBlobNotExist = errors.New("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)
+
+ has, err := e.Get(pb)
+ if err != nil {
+ return nil, false, err
+ }
+ if has {
+ return pb, 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
+}
+
+// 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 OUTER", "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() (int64, error) {
+ return db.GetEngine(db.DefaultContext).
+ SumInt(&PackageBlob{}, "size")
+}
diff --git a/models/packages/package_blob_upload.go b/models/packages/package_blob_upload.go
new file mode 100644
index 0000000000..635068f1d8
--- /dev/null
+++ b/models/packages/package_blob_upload.go
@@ -0,0 +1,81 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package packages
+
+import (
+ "context"
+ "errors"
+ "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 = errors.New("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_file.go b/models/packages/package_file.go
new file mode 100644
index 0000000000..df36467548
--- /dev/null
+++ b/models/packages/package_file.go
@@ -0,0 +1,201 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package packages
+
+import (
+ "context"
+ "errors"
+ "strconv"
+ "strings"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "xorm.io/builder"
+)
+
+func init() {
+ db.RegisterModel(new(PackageFile))
+}
+
+var (
+ // ErrDuplicatePackageFile indicates a duplicated package file error
+ ErrDuplicatePackageFile = errors.New("Package file does exist already")
+ // ErrPackageFileNotExist indicates a package file not exist error
+ ErrPackageFileNotExist = errors.New("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)
+
+ key := &PackageFile{
+ VersionID: pf.VersionID,
+ LowerName: pf.LowerName,
+ CompositeKey: pf.CompositeKey,
+ }
+
+ has, err := e.Get(key)
+ if err != nil {
+ return nil, err
+ }
+ if has {
+ return pf, 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{
+ VersionID: versionID,
+ LowerName: strings.ToLower(name),
+ CompositeKey: key,
+ }
+
+ has, err := db.GetEngine(ctx).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 string
+ VersionID int64
+ Query string
+ CompositeKey string
+ Properties map[string]string
+ OlderThan time.Duration
+ 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").
+ Join("INNER", "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()})
+ }
+
+ 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
+}
diff --git a/models/packages/package_property.go b/models/packages/package_property.go
new file mode 100644
index 0000000000..bf7dc346c6
--- /dev/null
+++ b/models/packages/package_property.go
@@ -0,0 +1,70 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package packages
+
+import (
+ "context"
+
+ "code.gitea.io/gitea/models/db"
+)
+
+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
+)
+
+// 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)
+}
+
+// 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
+}
diff --git a/models/packages/package_version.go b/models/packages/package_version.go
new file mode 100644
index 0000000000..f7c6d4dc58
--- /dev/null
+++ b/models/packages/package_version.go
@@ -0,0 +1,316 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package packages
+
+import (
+ "context"
+ "errors"
+ "strconv"
+ "strings"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "xorm.io/builder"
+)
+
+var (
+ // ErrDuplicatePackageVersion indicates a duplicated package version error
+ ErrDuplicatePackageVersion = errors.New("Package version does exist already")
+ // ErrPackageVersionNotExist indicates a package version not exist error
+ ErrPackageVersionNotExist = errors.New("Package version does not exist")
+)
+
+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 TEXT"`
+ 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)
+
+ key := &PackageVersion{
+ PackageID: pv.PackageID,
+ LowerVersion: pv.LowerVersion,
+ }
+
+ has, err := e.Get(key)
+ if err != nil {
+ return nil, err
+ }
+ if has {
+ return key, 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) {
+ var cond builder.Cond = builder.Eq{
+ "package.owner_id": ownerID,
+ "package.type": packageType,
+ "package.lower_name": strings.ToLower(name),
+ "package_version.is_internal": isInternal,
+ }
+ pv := &PackageVersion{
+ LowerVersion: strings.ToLower(version),
+ }
+ has, err := db.GetEngine(ctx).
+ Join("INNER", "package", "package.id = package_version.package_id").
+ Where(cond).
+ Get(pv)
+ if err != nil {
+ return nil, err
+ }
+ if !has {
+ return nil, ErrPackageNotExist
+ }
+
+ return pv, nil
+}
+
+// GetVersionsByPackageType gets all versions of a specific type
+func GetVersionsByPackageType(ctx context.Context, ownerID int64, packageType Type) ([]*PackageVersion, error) {
+ var cond builder.Cond = builder.Eq{
+ "package.owner_id": ownerID,
+ "package.type": packageType,
+ "package_version.is_internal": false,
+ }
+
+ pvs := make([]*PackageVersion, 0, 10)
+ return pvs, db.GetEngine(ctx).
+ Where(cond).
+ Join("INNER", "package", "package.id = package_version.package_id").
+ Find(&pvs)
+}
+
+// GetVersionsByPackageName gets all versions of a specific package
+func GetVersionsByPackageName(ctx context.Context, ownerID int64, packageType Type, name string) ([]*PackageVersion, error) {
+ var cond builder.Cond = builder.Eq{
+ "package.owner_id": ownerID,
+ "package.type": packageType,
+ "package.lower_name": strings.ToLower(name),
+ "package_version.is_internal": false,
+ }
+
+ pvs := make([]*PackageVersion, 0, 10)
+ return pvs, db.GetEngine(ctx).
+ Where(cond).
+ Join("INNER", "package", "package.id = package_version.package_id").
+ Find(&pvs)
+}
+
+// GetVersionsByFilename gets all versions which are linked to a filename
+func GetVersionsByFilename(ctx context.Context, ownerID int64, packageType Type, filename string) ([]*PackageVersion, error) {
+ var cond builder.Cond = builder.Eq{
+ "package.owner_id": ownerID,
+ "package.type": packageType,
+ "package_file.lower_name": strings.ToLower(filename),
+ "package_version.is_internal": false,
+ }
+
+ pvs := make([]*PackageVersion, 0, 10)
+ return pvs, db.GetEngine(ctx).
+ Where(cond).
+ Join("INNER", "package_file", "package_file.version_id = package_version.id").
+ Join("INNER", "package", "package.id = package_version.package_id").
+ Find(&pvs)
+}
+
+// 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,
+ })
+}
+
+// PackageSearchOptions are options for SearchXXX methods
+type PackageSearchOptions struct {
+ OwnerID int64
+ RepoID int64
+ Type string
+ PackageID int64
+ QueryName string
+ QueryVersion string
+ Properties map[string]string
+ Sort string
+ db.Paginator
+}
+
+func (opts *PackageSearchOptions) toConds() builder.Cond {
+ var cond builder.Cond = builder.Eq{"package_version.is_internal": false}
+
+ 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.QueryName != "" {
+ cond = cond.And(builder.Like{"package.lower_name", strings.ToLower(opts.QueryName)})
+ }
+ if opts.QueryVersion != "" {
+ cond = cond.And(builder.Like{"package_version.lower_version", strings.ToLower(opts.QueryVersion)})
+ }
+
+ 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"),
+ })
+ }
+
+ return cond
+}
+
+func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) {
+ switch opts.Sort {
+ case "alphabetically":
+ e.Asc("package.name")
+ case "reversealphabetically":
+ e.Desc("package.name")
+ case "highestversion":
+ e.Desc("package_version.version")
+ case "lowestversion":
+ e.Asc("package_version.version")
+ case "oldest":
+ e.Asc("package_version.created_unix")
+ default:
+ e.Desc("package_version.created_unix")
+ }
+}
+
+// SearchVersions gets all versions of packages matching the search options
+func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) {
+ sess := db.GetEngine(ctx).
+ Where(opts.toConds()).
+ Table("package_version").
+ Join("INNER", "package", "package.id = package_version.package_id")
+
+ 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) {
+ cond := opts.toConds().
+ And(builder.Expr("pv2.id IS NULL"))
+
+ sess := db.GetEngine(ctx).
+ Table("package_version").
+ Join("LEFT", "package_version pv2", "package_version.package_id = pv2.package_id AND (package_version.created_unix < pv2.created_unix OR (package_version.created_unix = pv2.created_unix AND package_version.id < pv2.id))").
+ Join("INNER", "package", "package.id = package_version.package_id").
+ Where(cond)
+
+ 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
+}
+
+// FindVersionsByPropertyNameAndValue gets all package versions which are associated with a specific property + value
+func FindVersionsByPropertyNameAndValue(ctx context.Context, packageID int64, name, value string) ([]*PackageVersion, error) {
+ var cond builder.Cond = builder.Eq{
+ "package_property.ref_type": PropertyTypeVersion,
+ "package_property.name": name,
+ "package_property.value": value,
+ "package_version.package_id": packageID,
+ "package_version.is_internal": false,
+ }
+
+ pvs := make([]*PackageVersion, 0, 5)
+ return pvs, db.GetEngine(ctx).
+ Where(cond).
+ Join("INNER", "package_property", "package_property.ref_id = package_version.id").
+ Find(&pvs)
+}