code.gitea.io/gitea@v1.22.3/models/packages/package_version.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package packages
     5  
     6  import (
     7  	"context"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"code.gitea.io/gitea/models/db"
    12  	"code.gitea.io/gitea/modules/optional"
    13  	"code.gitea.io/gitea/modules/timeutil"
    14  	"code.gitea.io/gitea/modules/util"
    15  
    16  	"xorm.io/builder"
    17  )
    18  
    19  // ErrDuplicatePackageVersion indicates a duplicated package version error
    20  var ErrDuplicatePackageVersion = util.NewAlreadyExistErrorf("package version already exists")
    21  
    22  func init() {
    23  	db.RegisterModel(new(PackageVersion))
    24  }
    25  
    26  // PackageVersion represents a package version
    27  type PackageVersion struct {
    28  	ID            int64              `xorm:"pk autoincr"`
    29  	PackageID     int64              `xorm:"UNIQUE(s) INDEX NOT NULL"`
    30  	CreatorID     int64              `xorm:"NOT NULL DEFAULT 0"`
    31  	Version       string             `xorm:"NOT NULL"`
    32  	LowerVersion  string             `xorm:"UNIQUE(s) INDEX NOT NULL"`
    33  	CreatedUnix   timeutil.TimeStamp `xorm:"created INDEX NOT NULL"`
    34  	IsInternal    bool               `xorm:"INDEX NOT NULL DEFAULT false"`
    35  	MetadataJSON  string             `xorm:"metadata_json LONGTEXT"`
    36  	DownloadCount int64              `xorm:"NOT NULL DEFAULT 0"`
    37  }
    38  
    39  // GetOrInsertVersion inserts a version. If the same version exist already ErrDuplicatePackageVersion is returned
    40  func GetOrInsertVersion(ctx context.Context, pv *PackageVersion) (*PackageVersion, error) {
    41  	e := db.GetEngine(ctx)
    42  
    43  	existing := &PackageVersion{}
    44  
    45  	has, err := e.Where(builder.Eq{
    46  		"package_id":    pv.PackageID,
    47  		"lower_version": pv.LowerVersion,
    48  	}).Get(existing)
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  	if has {
    53  		return existing, ErrDuplicatePackageVersion
    54  	}
    55  	if _, err = e.Insert(pv); err != nil {
    56  		return nil, err
    57  	}
    58  	return pv, nil
    59  }
    60  
    61  // UpdateVersion updates a version
    62  func UpdateVersion(ctx context.Context, pv *PackageVersion) error {
    63  	_, err := db.GetEngine(ctx).ID(pv.ID).Update(pv)
    64  	return err
    65  }
    66  
    67  // IncrementDownloadCounter increments the download counter of a version
    68  func IncrementDownloadCounter(ctx context.Context, versionID int64) error {
    69  	_, err := db.GetEngine(ctx).Exec("UPDATE `package_version` SET `download_count` = `download_count` + 1 WHERE `id` = ?", versionID)
    70  	return err
    71  }
    72  
    73  // GetVersionByID gets a version by id
    74  func GetVersionByID(ctx context.Context, versionID int64) (*PackageVersion, error) {
    75  	pv := &PackageVersion{}
    76  
    77  	has, err := db.GetEngine(ctx).ID(versionID).Get(pv)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  	if !has {
    82  		return nil, ErrPackageNotExist
    83  	}
    84  	return pv, nil
    85  }
    86  
    87  // GetVersionByNameAndVersion gets a version by name and version number
    88  func GetVersionByNameAndVersion(ctx context.Context, ownerID int64, packageType Type, name, version string) (*PackageVersion, error) {
    89  	return getVersionByNameAndVersion(ctx, ownerID, packageType, name, version, false)
    90  }
    91  
    92  // GetInternalVersionByNameAndVersion gets a version by name and version number
    93  func GetInternalVersionByNameAndVersion(ctx context.Context, ownerID int64, packageType Type, name, version string) (*PackageVersion, error) {
    94  	return getVersionByNameAndVersion(ctx, ownerID, packageType, name, version, true)
    95  }
    96  
    97  func getVersionByNameAndVersion(ctx context.Context, ownerID int64, packageType Type, name, version string, isInternal bool) (*PackageVersion, error) {
    98  	pvs, _, err := SearchVersions(ctx, &PackageSearchOptions{
    99  		OwnerID: ownerID,
   100  		Type:    packageType,
   101  		Name: SearchValue{
   102  			ExactMatch: true,
   103  			Value:      name,
   104  		},
   105  		Version: SearchValue{
   106  			ExactMatch: true,
   107  			Value:      version,
   108  		},
   109  		IsInternal: optional.Some(isInternal),
   110  		Paginator:  db.NewAbsoluteListOptions(0, 1),
   111  	})
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  	if len(pvs) == 0 {
   116  		return nil, ErrPackageNotExist
   117  	}
   118  	return pvs[0], nil
   119  }
   120  
   121  // GetVersionsByPackageType gets all versions of a specific type
   122  func GetVersionsByPackageType(ctx context.Context, ownerID int64, packageType Type) ([]*PackageVersion, error) {
   123  	pvs, _, err := SearchVersions(ctx, &PackageSearchOptions{
   124  		OwnerID:    ownerID,
   125  		Type:       packageType,
   126  		IsInternal: optional.Some(false),
   127  	})
   128  	return pvs, err
   129  }
   130  
   131  // GetVersionsByPackageName gets all versions of a specific package
   132  func GetVersionsByPackageName(ctx context.Context, ownerID int64, packageType Type, name string) ([]*PackageVersion, error) {
   133  	pvs, _, err := SearchVersions(ctx, &PackageSearchOptions{
   134  		OwnerID: ownerID,
   135  		Type:    packageType,
   136  		Name: SearchValue{
   137  			ExactMatch: true,
   138  			Value:      name,
   139  		},
   140  		IsInternal: optional.Some(false),
   141  	})
   142  	return pvs, err
   143  }
   144  
   145  // DeleteVersionByID deletes a version by id
   146  func DeleteVersionByID(ctx context.Context, versionID int64) error {
   147  	_, err := db.GetEngine(ctx).ID(versionID).Delete(&PackageVersion{})
   148  	return err
   149  }
   150  
   151  // HasVersionFileReferences checks if there are associated files
   152  func HasVersionFileReferences(ctx context.Context, versionID int64) (bool, error) {
   153  	return db.GetEngine(ctx).Get(&PackageFile{
   154  		VersionID: versionID,
   155  	})
   156  }
   157  
   158  // SearchValue describes a value to search
   159  // If ExactMatch is true, the field must match the value otherwise a LIKE search is performed.
   160  type SearchValue struct {
   161  	Value      string
   162  	ExactMatch bool
   163  }
   164  
   165  type VersionSort = string
   166  
   167  const (
   168  	SortNameAsc     VersionSort = "name_asc"
   169  	SortNameDesc    VersionSort = "name_desc"
   170  	SortVersionAsc  VersionSort = "version_asc"
   171  	SortVersionDesc VersionSort = "version_desc"
   172  	SortCreatedAsc  VersionSort = "created_asc"
   173  	SortCreatedDesc VersionSort = "created_desc"
   174  )
   175  
   176  // PackageSearchOptions are options for SearchXXX methods
   177  // All fields optional and are not used if they have their default value (nil, "", 0)
   178  type PackageSearchOptions struct {
   179  	OwnerID         int64
   180  	RepoID          int64
   181  	Type            Type
   182  	PackageID       int64
   183  	Name            SearchValue       // only results with the specific name are found
   184  	Version         SearchValue       // only results with the specific version are found
   185  	Properties      map[string]string // only results are found which contain all listed version properties with the specific value
   186  	IsInternal      optional.Option[bool]
   187  	HasFileWithName string                // only results are found which are associated with a file with the specific name
   188  	HasFiles        optional.Option[bool] // only results are found which have associated files
   189  	Sort            VersionSort
   190  	db.Paginator
   191  }
   192  
   193  func (opts *PackageSearchOptions) ToConds() builder.Cond {
   194  	cond := builder.NewCond()
   195  	if opts.IsInternal.Has() {
   196  		cond = builder.Eq{
   197  			"package_version.is_internal": opts.IsInternal.Value(),
   198  		}
   199  	}
   200  
   201  	if opts.OwnerID != 0 {
   202  		cond = cond.And(builder.Eq{"package.owner_id": opts.OwnerID})
   203  	}
   204  	if opts.RepoID != 0 {
   205  		cond = cond.And(builder.Eq{"package.repo_id": opts.RepoID})
   206  	}
   207  	if opts.Type != "" && opts.Type != "all" {
   208  		cond = cond.And(builder.Eq{"package.type": opts.Type})
   209  	}
   210  	if opts.PackageID != 0 {
   211  		cond = cond.And(builder.Eq{"package.id": opts.PackageID})
   212  	}
   213  	if opts.Name.Value != "" {
   214  		if opts.Name.ExactMatch {
   215  			cond = cond.And(builder.Eq{"package.lower_name": strings.ToLower(opts.Name.Value)})
   216  		} else {
   217  			cond = cond.And(builder.Like{"package.lower_name", strings.ToLower(opts.Name.Value)})
   218  		}
   219  	}
   220  	if opts.Version.Value != "" {
   221  		if opts.Version.ExactMatch {
   222  			cond = cond.And(builder.Eq{"package_version.lower_version": strings.ToLower(opts.Version.Value)})
   223  		} else {
   224  			cond = cond.And(builder.Like{"package_version.lower_version", strings.ToLower(opts.Version.Value)})
   225  		}
   226  	}
   227  
   228  	if len(opts.Properties) != 0 {
   229  		var propsCond builder.Cond = builder.Eq{
   230  			"package_property.ref_type": PropertyTypeVersion,
   231  		}
   232  		propsCond = propsCond.And(builder.Expr("package_property.ref_id = package_version.id"))
   233  
   234  		propsCondBlock := builder.NewCond()
   235  		for name, value := range opts.Properties {
   236  			propsCondBlock = propsCondBlock.Or(builder.Eq{
   237  				"package_property.name":  name,
   238  				"package_property.value": value,
   239  			})
   240  		}
   241  		propsCond = propsCond.And(propsCondBlock)
   242  
   243  		cond = cond.And(builder.Eq{
   244  			strconv.Itoa(len(opts.Properties)): builder.Select("COUNT(*)").Where(propsCond).From("package_property"),
   245  		})
   246  	}
   247  
   248  	if opts.HasFileWithName != "" {
   249  		fileCond := builder.Expr("package_file.version_id = package_version.id").And(builder.Eq{"package_file.lower_name": strings.ToLower(opts.HasFileWithName)})
   250  
   251  		cond = cond.And(builder.Exists(builder.Select("package_file.id").From("package_file").Where(fileCond)))
   252  	}
   253  
   254  	if opts.HasFiles.Has() {
   255  		filesCond := builder.Exists(builder.Select("package_file.id").From("package_file").Where(builder.Expr("package_file.version_id = package_version.id")))
   256  
   257  		if !opts.HasFiles.Value() {
   258  			filesCond = builder.Not{filesCond}
   259  		}
   260  
   261  		cond = cond.And(filesCond)
   262  	}
   263  
   264  	return cond
   265  }
   266  
   267  func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) {
   268  	switch opts.Sort {
   269  	case SortNameAsc:
   270  		e.Asc("package.name")
   271  	case SortNameDesc:
   272  		e.Desc("package.name")
   273  	case SortVersionDesc:
   274  		e.Desc("package_version.version")
   275  	case SortVersionAsc:
   276  		e.Asc("package_version.version")
   277  	case SortCreatedAsc:
   278  		e.Asc("package_version.created_unix")
   279  	default:
   280  		e.Desc("package_version.created_unix")
   281  	}
   282  
   283  	// Sort by id for stable order with duplicates in the other field
   284  	e.Asc("package_version.id")
   285  }
   286  
   287  // SearchVersions gets all versions of packages matching the search options
   288  func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) {
   289  	sess := db.GetEngine(ctx).
   290  		Select("package_version.*").
   291  		Table("package_version").
   292  		Join("INNER", "package", "package.id = package_version.package_id").
   293  		Where(opts.ToConds())
   294  
   295  	opts.configureOrderBy(sess)
   296  
   297  	if opts.Paginator != nil {
   298  		sess = db.SetSessionPagination(sess, opts)
   299  	}
   300  
   301  	pvs := make([]*PackageVersion, 0, 10)
   302  	count, err := sess.FindAndCount(&pvs)
   303  	return pvs, count, err
   304  }
   305  
   306  // SearchLatestVersions gets the latest version of every package matching the search options
   307  func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) {
   308  	in := builder.
   309  		Select("MAX(package_version.id)").
   310  		From("package_version").
   311  		InnerJoin("package", "package.id = package_version.package_id").
   312  		Where(opts.ToConds()).
   313  		GroupBy("package_version.package_id")
   314  
   315  	sess := db.GetEngine(ctx).
   316  		Select("package_version.*").
   317  		Table("package_version").
   318  		Join("INNER", "package", "package.id = package_version.package_id").
   319  		Where(builder.In("package_version.id", in))
   320  
   321  	opts.configureOrderBy(sess)
   322  
   323  	if opts.Paginator != nil {
   324  		sess = db.SetSessionPagination(sess, opts)
   325  	}
   326  
   327  	pvs := make([]*PackageVersion, 0, 10)
   328  	count, err := sess.FindAndCount(&pvs)
   329  	return pvs, count, err
   330  }
   331  
   332  // ExistVersion checks if a version matching the search options exist
   333  func ExistVersion(ctx context.Context, opts *PackageSearchOptions) (bool, error) {
   334  	return db.GetEngine(ctx).
   335  		Where(opts.ToConds()).
   336  		Table("package_version").
   337  		Join("INNER", "package", "package.id = package_version.package_id").
   338  		Exist(new(PackageVersion))
   339  }
   340  
   341  // CountVersions counts all versions of packages matching the search options
   342  func CountVersions(ctx context.Context, opts *PackageSearchOptions) (int64, error) {
   343  	return db.GetEngine(ctx).
   344  		Where(opts.ToConds()).
   345  		Table("package_version").
   346  		Join("INNER", "package", "package.id = package_version.package_id").
   347  		Count(new(PackageVersion))
   348  }