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 }