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>
This commit is contained in:
parent
2bce1ea986
commit
1d332342db
197 changed files with 18563 additions and 55 deletions
|
@ -58,6 +58,21 @@ func (err ErrUserHasOrgs) Error() string {
|
|||
return fmt.Sprintf("user still has membership of organizations [uid: %d]", err.UID)
|
||||
}
|
||||
|
||||
// ErrUserOwnPackages notifies that the user (still) owns the packages.
|
||||
type ErrUserOwnPackages struct {
|
||||
UID int64
|
||||
}
|
||||
|
||||
// IsErrUserOwnPackages checks if an error is an ErrUserOwnPackages.
|
||||
func IsErrUserOwnPackages(err error) bool {
|
||||
_, ok := err.(ErrUserOwnPackages)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserOwnPackages) Error() string {
|
||||
return fmt.Sprintf("user still has ownership of packages [uid: %d]", err.UID)
|
||||
}
|
||||
|
||||
// __ __.__ __ .__
|
||||
// / \ / \__| | _|__|
|
||||
// \ \/\/ / | |/ / |
|
||||
|
|
|
@ -378,6 +378,8 @@ var migrations = []Migration{
|
|||
|
||||
// v211 -> v212
|
||||
NewMigration("Create ForeignReference table", createForeignReferenceTable),
|
||||
// v212 -> v213
|
||||
NewMigration("Add package tables", addPackageTables),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current db version
|
||||
|
|
94
models/migrations/v212.go
Normal file
94
models/migrations/v212.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func addPackageTables(x *xorm.Engine) error {
|
||||
type Package struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OwnerID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
|
||||
RepoID int64 `xorm:"INDEX"`
|
||||
Type string `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"`
|
||||
}
|
||||
|
||||
if err := x.Sync2(new(Package)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
if err := x.Sync2(new(PackageVersion)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type PackageProperty struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RefType int64 `xorm:"INDEX NOT NULL"`
|
||||
RefID int64 `xorm:"INDEX NOT NULL"`
|
||||
Name string `xorm:"INDEX NOT NULL"`
|
||||
Value string `xorm:"TEXT NOT NULL"`
|
||||
}
|
||||
|
||||
if err := x.Sync2(new(PackageProperty)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
if err := x.Sync2(new(PackageFile)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
if err := x.Sync2(new(PackageBlob)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
return x.Sync2(new(PackageBlobUpload))
|
||||
}
|
171
models/packages/conan/references.go
Normal file
171
models/packages/conan/references.go
Normal file
|
@ -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
|
||||
}
|
149
models/packages/conan/search.go
Normal file
149
models/packages/conan/search.go
Normal file
|
@ -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
|
||||
}
|
10
models/packages/container/const.go
Normal file
10
models/packages/container/const.go
Normal file
|
@ -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"
|
||||
)
|
227
models/packages/container/search.go
Normal file
227
models/packages/container/search.go
Normal file
|
@ -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)
|
||||
}
|
192
models/packages/descriptor.go
Normal file
192
models/packages/descriptor.go
Normal file
|
@ -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
|
||||
}
|
213
models/packages/package.go
Normal file
213
models/packages/package.go
Normal file
|
@ -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{})
|
||||
}
|
85
models/packages/package_blob.go
Normal file
85
models/packages/package_blob.go
Normal file
|
@ -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")
|
||||
}
|
81
models/packages/package_blob_upload.go
Normal file
81
models/packages/package_blob_upload.go
Normal file
|
@ -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)
|
||||
}
|
201
models/packages/package_file.go
Normal file
201
models/packages/package_file.go
Normal file
|
@ -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
|
||||
}
|
70
models/packages/package_property.go
Normal file
70
models/packages/package_property.go
Normal file
|
@ -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
|
||||
}
|
316
models/packages/package_version.go
Normal file
316
models/packages/package_version.go
Normal file
|
@ -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)
|
||||
}
|
|
@ -26,7 +26,7 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
reservedRepoNames = []string{".", ".."}
|
||||
reservedRepoNames = []string{".", "..", "-"}
|
||||
reservedRepoPatterns = []string{"*.git", "*.wiki", "*.rss", "*.atom"}
|
||||
)
|
||||
|
||||
|
|
|
@ -95,6 +95,8 @@ func MainTest(m *testing.M, pathToGiteaRoot string, fixtureFiles ...string) {
|
|||
|
||||
setting.RepoArchive.Storage.Path = filepath.Join(setting.AppDataPath, "repo-archive")
|
||||
|
||||
setting.Packages.Storage.Path = filepath.Join(setting.AppDataPath, "packages")
|
||||
|
||||
if err = storage.Init(); err != nil {
|
||||
fatalTestError("storage.Init: %v\n", err)
|
||||
}
|
||||
|
|
|
@ -605,6 +605,7 @@ var (
|
|||
"stars",
|
||||
"template",
|
||||
"user",
|
||||
"v2",
|
||||
}
|
||||
|
||||
reservedUserPatterns = []string{"*.keys", "*.gpg", "*.rss", "*.atom"}
|
||||
|
|
|
@ -49,6 +49,7 @@ const (
|
|||
HookEventPullRequestSync HookEventType = "pull_request_sync"
|
||||
HookEventRepository HookEventType = "repository"
|
||||
HookEventRelease HookEventType = "release"
|
||||
HookEventPackage HookEventType = "package"
|
||||
)
|
||||
|
||||
// Event returns the HookEventType as an event string
|
||||
|
|
|
@ -134,6 +134,7 @@ type HookEvents struct {
|
|||
PullRequestSync bool `json:"pull_request_sync"`
|
||||
Repository bool `json:"repository"`
|
||||
Release bool `json:"release"`
|
||||
Package bool `json:"package"`
|
||||
}
|
||||
|
||||
// HookEvent represents events that will delivery hook.
|
||||
|
@ -339,6 +340,12 @@ func (w *Webhook) HasRepositoryEvent() bool {
|
|||
(w.ChooseEvents && w.HookEvents.Repository)
|
||||
}
|
||||
|
||||
// HasPackageEvent returns if hook enabled package event.
|
||||
func (w *Webhook) HasPackageEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.Package)
|
||||
}
|
||||
|
||||
// EventCheckers returns event checkers
|
||||
func (w *Webhook) EventCheckers() []struct {
|
||||
Has func() bool
|
||||
|
@ -368,6 +375,7 @@ func (w *Webhook) EventCheckers() []struct {
|
|||
{w.HasPullRequestSyncEvent, HookEventPullRequestSync},
|
||||
{w.HasRepositoryEvent, HookEventRepository},
|
||||
{w.HasReleaseEvent, HookEventRelease},
|
||||
{w.HasPackageEvent, HookEventPackage},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -72,6 +72,7 @@ func TestWebhook_EventsArray(t *testing.T) {
|
|||
"pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone",
|
||||
"pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected",
|
||||
"pull_request_review_comment", "pull_request_sync", "repository", "release",
|
||||
"package",
|
||||
},
|
||||
(&Webhook{
|
||||
HookEvent: &HookEvent{SendEverything: true},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue