Add package cleanup rules.

This commit is contained in:
KN4CK3R 2022-10-31 20:04:07 +00:00
parent 9a70a12a34
commit 1c160956da
23 changed files with 920 additions and 1 deletions

View file

@ -425,6 +425,8 @@ var migrations = []Migration{
NewMigration("Add ConfidentialClient column (default true) to OAuth2Application table", addConfidentialClientColumnToOAuth2ApplicationTable),
// v231 -> v232
NewMigration("Add index for hook_task", addIndexForHookTask),
// v232 -> v233
NewMigration("Add package cleanup rule table", createPackageCleanupRuleTable),
}
// GetCurrentDBVersion returns the current db version

29
models/migrations/v232.go Normal file
View file

@ -0,0 +1,29 @@
// 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 createPackageCleanupRuleTable(x *xorm.Engine) error {
type PackageCleanupRule struct {
ID int64 `xorm:"pk autoincr"`
Enabled bool `xorm:"INDEX NOT NULL DEFAULT false"`
OwnerID int64 `xorm:"UNIQUE(s) INDEX NOT NULL DEFAULT 0"`
Type string `xorm:"UNIQUE(s) INDEX NOT NULL"`
KeepCount int `xorm:"NOT NULL DEFAULT 0"`
KeepPattern string `xorm:"NOT NULL DEFAULT ''"`
RemoveDays int `xorm:"NOT NULL DEFAULT 0"`
RemovePattern string `xorm:"NOT NULL DEFAULT ''"`
MatchFullName bool `xorm:"NOT NULL DEFAULT false"`
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL DEFAULT 0"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL DEFAULT 0"`
}
return x.Sync2(new(PackageCleanupRule))
}

View file

@ -45,6 +45,21 @@ const (
TypeVagrant Type = "vagrant"
)
var TypeList = []Type{
TypeComposer,
TypeConan,
TypeContainer,
TypeGeneric,
TypeHelm,
TypeMaven,
TypeNpm,
TypeNuGet,
TypePub,
TypePyPI,
TypeRubyGems,
TypeVagrant,
}
// Name gets the name of the package type
func (pt Type) Name() string {
switch pt {

View file

@ -0,0 +1,111 @@
// 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"
"fmt"
"regexp"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/builder"
)
var ErrPackageCleanupRuleNotExist = errors.New("Package blob does not exist")
func init() {
db.RegisterModel(new(PackageCleanupRule))
}
// PackageCleanupRule represents a rule which describes when to clean up package versions
type PackageCleanupRule struct {
ID int64 `xorm:"pk autoincr"`
Enabled bool `xorm:"INDEX NOT NULL DEFAULT false"`
OwnerID int64 `xorm:"UNIQUE(s) INDEX NOT NULL DEFAULT 0"`
Type Type `xorm:"UNIQUE(s) INDEX NOT NULL"`
KeepCount int `xorm:"NOT NULL DEFAULT 0"`
KeepPattern string `xorm:"NOT NULL DEFAULT ''"`
KeepPatternMatcher *regexp.Regexp `xorm:"-"`
RemoveDays int `xorm:"NOT NULL DEFAULT 0"`
RemovePattern string `xorm:"NOT NULL DEFAULT ''"`
RemovePatternMatcher *regexp.Regexp `xorm:"-"`
MatchFullName bool `xorm:"NOT NULL DEFAULT false"`
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL DEFAULT 0"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL DEFAULT 0"`
}
func (pcr *PackageCleanupRule) CompiledPattern() error {
if pcr.KeepPatternMatcher != nil || pcr.RemovePatternMatcher != nil {
return nil
}
if pcr.KeepPattern != "" {
var err error
pcr.KeepPatternMatcher, err = regexp.Compile(fmt.Sprintf(`(?i)\A%s\z`, pcr.KeepPattern))
if err != nil {
return err
}
}
if pcr.RemovePattern != "" {
var err error
pcr.RemovePatternMatcher, err = regexp.Compile(fmt.Sprintf(`(?i)\A%s\z`, pcr.RemovePattern))
if err != nil {
return err
}
}
return nil
}
func InsertCleanupRule(ctx context.Context, pcr *PackageCleanupRule) (*PackageCleanupRule, error) {
_, err := db.GetEngine(ctx).Insert(pcr)
return pcr, err
}
func GetCleanupRuleByID(ctx context.Context, id int64) (*PackageCleanupRule, error) {
pcr := &PackageCleanupRule{}
has, err := db.GetEngine(ctx).ID(id).Get(pcr)
if err != nil {
return nil, err
}
if !has {
return nil, ErrPackageCleanupRuleNotExist
}
return pcr, nil
}
func UpdateCleanupRule(ctx context.Context, pcr *PackageCleanupRule) error {
_, err := db.GetEngine(ctx).ID(pcr.ID).AllCols().Update(pcr)
return err
}
func GetCleanupRulesByOwner(ctx context.Context, ownerID int64) ([]*PackageCleanupRule, error) {
pcrs := make([]*PackageCleanupRule, 0, 10)
return pcrs, db.GetEngine(ctx).Where("owner_id = ?", ownerID).Find(&pcrs)
}
func DeleteCleanupRuleByID(ctx context.Context, ruleID int64) error {
_, err := db.GetEngine(ctx).ID(ruleID).Delete(&PackageCleanupRule{})
return err
}
func HasOwnerCleanupRuleForPackageType(ctx context.Context, ownerID int64, packageType Type) (bool, error) {
return db.GetEngine(ctx).
Where("owner_id = ? AND type = ?", ownerID, packageType).
Exist(&PackageCleanupRule{})
}
func IterateEnabledCleanupRules(ctx context.Context, callback func(context.Context, *PackageCleanupRule) error) error {
return db.Iterate(
ctx,
builder.Eq{"enabled": true},
callback,
)
}

View file

@ -86,6 +86,9 @@ remove = Remove
remove_all = Remove All
edit = Edit
enabled = Enabled
disabled = Disabled
copy = Copy
copy_url = Copy URL
copy_branch = Copy branch name
@ -3177,3 +3180,23 @@ settings.delete.description = Deleting a package is permanent and cannot be undo
settings.delete.notice = You are about to delete %s (%s). This operation is irreversible, are you sure?
settings.delete.success = The package has been deleted.
settings.delete.error = Failed to delete the package.
owner.settings.cleanuprules.title = Manage Cleanup Rules
owner.settings.cleanuprules.add = Add Cleanup Rule
owner.settings.cleanuprules.edit = Edit Cleanup Rule
owner.settings.cleanuprules.none = No cleanup rules available. Read the docs to learn more.
owner.settings.cleanuprules.preview = Cleanup Rule Preview
owner.settings.cleanuprules.preview.overview = %d packages are scheduled to be removed.
owner.settings.cleanuprules.preview.none = Cleanup rule does not match any packages.
owner.settings.cleanuprules.enabled = Enabled
owner.settings.cleanuprules.pattern_full_match = Apply pattern to full package name
owner.settings.cleanuprules.keep.title = Versions that match these rules are kept, even if they match a removal rule below.
owner.settings.cleanuprules.keep.count = Keep the most recent
owner.settings.cleanuprules.keep.count.1 = 1 version per package
owner.settings.cleanuprules.keep.count.n = %d versions per package
owner.settings.cleanuprules.keep.pattern = Keep versions matching
owner.settings.cleanuprules.keep.pattern.container = The <code>latest</code> version is always kept for Container packages.
owner.settings.cleanuprules.remove.title = Versions that match these rules are removed, unless a rule above says to keep them.
owner.settings.cleanuprules.remove.days = Remove versions older than
owner.settings.cleanuprules.remove.pattern = Remove versions matching
owner.settings.cleanuprules.success.update = Cleanup rule has been updated.
owner.settings.cleanuprules.success.delete = Cleanup rule has been deleted.

View file

@ -0,0 +1,87 @@
// 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 org
import (
"fmt"
"net/http"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
shared "code.gitea.io/gitea/routers/web/shared/packages"
)
const (
tplSettingsPackages base.TplName = "org/settings/packages"
tplSettingsPackagesRuleEdit base.TplName = "org/settings/packages_cleanup_rules_edit"
tplSettingsPackagesRulePreview base.TplName = "org/settings/packages_cleanup_rules_preview"
)
func Packages(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsPackages"] = true
shared.SetPackagesContext(ctx, ctx.ContextUser)
ctx.HTML(http.StatusOK, tplSettingsPackages)
}
func PackagesRuleAdd(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsPackages"] = true
shared.SetRuleAddContext(ctx)
ctx.HTML(http.StatusOK, tplSettingsPackagesRuleEdit)
}
func PackagesRuleEdit(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsPackages"] = true
shared.SetRuleEditContext(ctx, ctx.ContextUser)
ctx.HTML(http.StatusOK, tplSettingsPackagesRuleEdit)
}
func PackagesRuleAddPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsPackages"] = true
shared.PerformRuleAddPost(
ctx,
ctx.ContextUser,
fmt.Sprintf("%s/org/%s/settings/packages", setting.AppSubURL, ctx.ContextUser.Name),
tplSettingsPackagesRuleEdit,
)
}
func PackagesRuleEditPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsPackages"] = true
shared.PerformRuleEditPost(
ctx,
ctx.ContextUser,
fmt.Sprintf("%s/org/%s/settings/packages", setting.AppSubURL, ctx.ContextUser.Name),
tplSettingsPackagesRuleEdit,
)
}
func PackagesRulePreview(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsPackages"] = true
shared.SetRulePreviewContext(ctx, ctx.ContextUser)
ctx.HTML(http.StatusOK, tplSettingsPackagesRulePreview)
}

View file

@ -0,0 +1,232 @@
// 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 (
"fmt"
"net/http"
"time"
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
container_service "code.gitea.io/gitea/services/packages/container"
)
const (
tplSettingsPackages base.TplName = "user/settings/packages"
tplSettingsPackagesRuleEdit base.TplName = "user/settings/packages_cleanup_rules_edit"
tplSettingsPackagesRulePreview base.TplName = "user/settings/packages_cleanup_rules_preview"
)
func SetPackagesContext(ctx *context.Context, owner *user_model.User) {
pcrs, err := packages_model.GetCleanupRulesByOwner(ctx, owner.ID)
if err != nil {
ctx.ServerError("GetCleanupRulesByOwner", err)
return
}
ctx.Data["CleanupRules"] = pcrs
}
func SetRuleAddContext(ctx *context.Context) {
setRuleEditContext(ctx, nil)
}
func SetRuleEditContext(ctx *context.Context, owner *user_model.User) {
pcr := getCleanupRuleByContext(ctx, owner)
if pcr == nil {
return
}
setRuleEditContext(ctx, pcr)
}
func setRuleEditContext(ctx *context.Context, pcr *packages_model.PackageCleanupRule) {
ctx.Data["IsEditRule"] = pcr != nil
if pcr == nil {
pcr = &packages_model.PackageCleanupRule{}
}
ctx.Data["CleanupRule"] = pcr
ctx.Data["AvailableTypes"] = packages_model.TypeList
}
func PerformRuleAddPost(ctx *context.Context, owner *user_model.User, redirectURL string, template base.TplName) {
performRuleEditPost(ctx, owner, nil, redirectURL, template)
}
func PerformRuleEditPost(ctx *context.Context, owner *user_model.User, redirectURL string, template base.TplName) {
pcr := getCleanupRuleByContext(ctx, owner)
if pcr == nil {
return
}
form := web.GetForm(ctx).(*forms.PackageCleanupRuleForm)
if form.Action == "remove" {
if err := packages_model.DeleteCleanupRuleByID(ctx, pcr.ID); err != nil {
ctx.ServerError("DeleteCleanupRuleByID", err)
return
}
ctx.Flash.Success(ctx.Tr("packages.owner.settings.cleanuprules.success.delete"))
ctx.Redirect(redirectURL)
} else {
performRuleEditPost(ctx, owner, pcr, redirectURL, template)
}
}
func performRuleEditPost(ctx *context.Context, owner *user_model.User, pcr *packages_model.PackageCleanupRule, redirectURL string, template base.TplName) {
isEditRule := pcr != nil
if pcr == nil {
pcr = &packages_model.PackageCleanupRule{}
}
form := web.GetForm(ctx).(*forms.PackageCleanupRuleForm)
pcr.Enabled = form.Enabled
pcr.OwnerID = owner.ID
pcr.KeepCount = form.KeepCount
pcr.KeepPattern = form.KeepPattern
pcr.RemoveDays = form.RemoveDays
pcr.RemovePattern = form.RemovePattern
pcr.MatchFullName = form.MatchFullName
ctx.Data["IsEditRule"] = isEditRule
ctx.Data["CleanupRule"] = pcr
ctx.Data["AvailableTypes"] = packages_model.TypeList
if ctx.HasError() {
ctx.HTML(http.StatusOK, template)
return
}
if isEditRule {
if err := packages_model.UpdateCleanupRule(ctx, pcr); err != nil {
ctx.ServerError("UpdateCleanupRule", err)
return
}
} else {
pcr.Type = packages_model.Type(form.Type)
if has, err := packages_model.HasOwnerCleanupRuleForPackageType(ctx, owner.ID, pcr.Type); err != nil {
ctx.ServerError("HasOwnerCleanupRuleForPackageType", err)
return
} else if has {
ctx.Data["Err_Type"] = true
ctx.HTML(http.StatusOK, template)
return
}
var err error
if pcr, err = packages_model.InsertCleanupRule(ctx, pcr); err != nil {
ctx.ServerError("InsertCleanupRule", err)
return
}
}
ctx.Flash.Success(ctx.Tr("packages.owner.settings.cleanuprules.success.update"))
ctx.Redirect(fmt.Sprintf("%s/rules/%d", redirectURL, pcr.ID))
}
func SetRulePreviewContext(ctx *context.Context, owner *user_model.User) {
pcr := getCleanupRuleByContext(ctx, owner)
if pcr == nil {
return
}
if err := pcr.CompiledPattern(); err != nil {
ctx.ServerError("CompiledPattern", err)
return
}
olderThan := time.Now().AddDate(0, 0, -pcr.RemoveDays)
packages, err := packages_model.GetPackagesByType(ctx, pcr.OwnerID, pcr.Type)
if err != nil {
ctx.ServerError("GetPackagesByType", err)
return
}
versionsToRemove := make([]*packages_model.PackageDescriptor, 0, 10)
for _, p := range packages {
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
PackageID: p.ID,
IsInternal: util.OptionalBoolFalse,
Sort: packages_model.SortCreatedDesc,
Paginator: db.NewAbsoluteListOptions(pcr.KeepCount, 200),
})
if err != nil {
ctx.ServerError("SearchVersions", err)
return
}
for _, pv := range pvs {
if skip, err := container_service.ShouldBeSkipped(pcr, p, pv); err != nil {
ctx.ServerError("ShouldBeSkipped", err)
return
} else if skip {
continue
}
toMatch := pv.LowerVersion
if pcr.MatchFullName {
toMatch = p.LowerName + "/" + pv.LowerVersion
}
if pcr.KeepPatternMatcher != nil && pcr.KeepPatternMatcher.MatchString(toMatch) {
continue
}
if pv.CreatedUnix.AsLocalTime().After(olderThan) {
continue
}
if pcr.RemovePatternMatcher != nil && !pcr.RemovePatternMatcher.MatchString(toMatch) {
continue
}
pd, err := packages_model.GetPackageDescriptor(ctx, pv)
if err != nil {
ctx.ServerError("GetPackageDescriptor", err)
return
}
versionsToRemove = append(versionsToRemove, pd)
}
}
ctx.Data["CleanupRule"] = pcr
ctx.Data["VersionsToRemove"] = versionsToRemove
}
func getCleanupRuleByContext(ctx *context.Context, owner *user_model.User) *packages_model.PackageCleanupRule {
id := ctx.FormInt64("id")
if id == 0 {
id = ctx.ParamsInt64("id")
}
pcr, err := packages_model.GetCleanupRuleByID(ctx, id)
if err != nil {
if err == packages_model.ErrPackageCleanupRuleNotExist {
ctx.NotFound("", err)
} else {
ctx.ServerError("GetCleanupRuleByID", err)
}
return nil
}
if pcr != nil && pcr.OwnerID == owner.ID {
return pcr
}
ctx.NotFound("", fmt.Errorf("PackageCleanupRule[%v] not associated to owner %v", id, owner))
return nil
}

View file

@ -0,0 +1,80 @@
// 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 setting
import (
"net/http"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
shared "code.gitea.io/gitea/routers/web/shared/packages"
)
const (
tplSettingsPackages base.TplName = "user/settings/packages"
tplSettingsPackagesRuleEdit base.TplName = "user/settings/packages_cleanup_rules_edit"
tplSettingsPackagesRulePreview base.TplName = "user/settings/packages_cleanup_rules_preview"
)
func Packages(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsSettingsPackages"] = true
shared.SetPackagesContext(ctx, ctx.Doer)
ctx.HTML(http.StatusOK, tplSettingsPackages)
}
func PackagesRuleAdd(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsSettingsPackages"] = true
shared.SetRuleAddContext(ctx)
ctx.HTML(http.StatusOK, tplSettingsPackagesRuleEdit)
}
func PackagesRuleEdit(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsSettingsPackages"] = true
shared.SetRuleEditContext(ctx, ctx.Doer)
ctx.HTML(http.StatusOK, tplSettingsPackagesRuleEdit)
}
func PackagesRuleAddPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsPackages"] = true
shared.PerformRuleAddPost(
ctx,
ctx.Doer,
setting.AppSubURL+"/user/settings/packages",
tplSettingsPackagesRuleEdit,
)
}
func PackagesRuleEditPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsSettingsPackages"] = true
shared.PerformRuleEditPost(
ctx,
ctx.Doer,
setting.AppSubURL+"/user/settings/packages",
tplSettingsPackagesRuleEdit,
)
}
func PackagesRulePreview(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["PageIsSettingsPackages"] = true
shared.SetRulePreviewContext(ctx, ctx.Doer)
ctx.HTML(http.StatusOK, tplSettingsPackagesRulePreview)
}

View file

@ -443,6 +443,20 @@ func RegisterRoutes(m *web.Route) {
m.Combo("/keys").Get(user_setting.Keys).
Post(bindIgnErr(forms.AddKeyForm{}), user_setting.KeysPost)
m.Post("/keys/delete", user_setting.DeleteKey)
m.Group("/packages", func() {
m.Get("", user_setting.Packages)
m.Group("/rules", func() {
m.Group("/add", func() {
m.Get("", user_setting.PackagesRuleAdd)
m.Post("", bindIgnErr(forms.PackageCleanupRuleForm{}), user_setting.PackagesRuleAddPost)
})
m.Group("/{id}", func() {
m.Get("", user_setting.PackagesRuleEdit)
m.Post("", bindIgnErr(forms.PackageCleanupRuleForm{}), user_setting.PackagesRuleEditPost)
m.Get("/preview", user_setting.PackagesRulePreview)
})
})
})
m.Get("/organization", user_setting.Organization)
m.Get("/repos", user_setting.Repos)
m.Post("/repos/unadopted", user_setting.AdoptOrDeleteRepository)
@ -751,6 +765,21 @@ func RegisterRoutes(m *web.Route) {
})
m.Route("/delete", "GET,POST", org.SettingsDelete)
m.Group("/packages", func() {
m.Get("", org.Packages)
m.Group("/rules", func() {
m.Group("/add", func() {
m.Get("", org.PackagesRuleAdd)
m.Post("", bindIgnErr(forms.PackageCleanupRuleForm{}), org.PackagesRuleAddPost)
})
m.Group("/{id}", func() {
m.Get("", org.PackagesRuleEdit)
m.Post("", bindIgnErr(forms.PackageCleanupRuleForm{}), org.PackagesRuleEditPost)
m.Get("/preview", org.PackagesRulePreview)
})
})
})
}, func(ctx *context.Context) {
ctx.Data["EnableOAuth2"] = setting.OAuth2.Enable
})

View file

@ -0,0 +1,31 @@
// 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 forms
import (
"net/http"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/web/middleware"
"gitea.com/go-chi/binding"
)
type PackageCleanupRuleForm struct {
ID int64
Enabled bool
Type string `binding:"Required;In(composer,conan,container,generic,helm,maven,npm,nuget,pub,pypi,rubygems,vagrant)"`
KeepCount int `binding:"In(0,1,5,10,25,50,100)"`
KeepPattern string `binding:"RegexPattern"`
RemoveDays int `binding:"In(0,7,14,30,60,90,180)"`
RemovePattern string `binding:"RegexPattern"`
MatchFullName bool
Action string `binding:"Required;In(save,remove)"`
}
func (f *PackageCleanupRuleForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}

View file

@ -82,6 +82,10 @@ func cleanupExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) e
return nil
}
func ShouldBeSkipped(pcr *packages_model.PackageCleanupRule, p *packages_model.Package, pv *packages_model.PackageVersion) (bool, error) {
return pv.LowerVersion == "latest", nil
}
// UpdateRepositoryNames updates the repository name property for all packages of the specific owner
func UpdateRepositoryNames(ctx context.Context, owner *user_model.User, newOwnerName string) error {
ps, err := packages_model.GetPackagesByType(ctx, owner.ID, packages_model.TypeContainer)

View file

@ -352,13 +352,75 @@ func DeletePackageFile(ctx context.Context, pf *packages_model.PackageFile) erro
}
// Cleanup removes expired package data
func Cleanup(unused context.Context, olderThan time.Duration) error {
func Cleanup(_ context.Context, olderThan time.Duration) error {
ctx, committer, err := db.TxContext()
if err != nil {
return err
}
defer committer.Close()
err = packages_model.IterateEnabledCleanupRules(ctx, func(ctx context.Context, pcr *packages_model.PackageCleanupRule) error {
if err := pcr.CompiledPattern(); err != nil {
return fmt.Errorf("CleanupRule [%d]: CompilePattern failed: %w", pcr.ID, err)
}
olderThan := time.Now().AddDate(0, 0, -pcr.RemoveDays)
packages, err := packages_model.GetPackagesByType(ctx, pcr.OwnerID, pcr.Type)
if err != nil {
return fmt.Errorf("CleanupRule [%d]: GetPackagesByType failed: %w", pcr.ID, err)
}
for _, p := range packages {
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
PackageID: p.ID,
IsInternal: util.OptionalBoolFalse,
Sort: packages_model.SortCreatedDesc,
Paginator: db.NewAbsoluteListOptions(pcr.KeepCount, 200),
})
if err != nil {
return fmt.Errorf("CleanupRule [%d]: SearchVersions failed: %w", pcr.ID, err)
}
for _, pv := range pvs {
if skip, err := container_service.ShouldBeSkipped(pcr, p, pv); err != nil {
return fmt.Errorf("CleanupRule [%d]: container.ShouldBeSkipped failed: %w", pcr.ID, err)
} else if skip {
log.Debug("Rule[%d]: keep '%s/%s' (container)", pcr.ID, p.Name, pv.Version)
continue
}
toMatch := pv.LowerVersion
if pcr.MatchFullName {
toMatch = p.LowerName + "/" + pv.LowerVersion
}
if pcr.KeepPatternMatcher != nil && pcr.KeepPatternMatcher.MatchString(toMatch) {
log.Debug("Rule[%d]: keep '%s/%s' (keep pattern)", pcr.ID, p.Name, pv.Version)
continue
}
if pv.CreatedUnix.AsLocalTime().After(olderThan) {
log.Debug("Rule[%d]: keep '%s/%s' (remove days)", pcr.ID, p.Name, pv.Version)
continue
}
if pcr.RemovePatternMatcher != nil && !pcr.RemovePatternMatcher.MatchString(toMatch) {
log.Debug("Rule[%d]: do not remove '%s/%s'", pcr.ID, p.Name, pv.Version)
continue
}
log.Debug("Rule[%d]: remove '%s/%s'", pcr.ID, p.Name, pv.Version)
if err := DeletePackageVersionAndReferences(ctx, pv); err != nil {
return fmt.Errorf("CleanupRule [%d]: DeletePackageVersionAndReferences failed: %w", pcr.ID, err)
}
}
}
return nil
})
if err != nil {
log.Error("%#v", err)
return err
}
if err := container_service.Cleanup(ctx, olderThan); err != nil {
return err
}

View file

@ -17,6 +17,9 @@
{{.locale.Tr "settings.applications"}}
</a>
{{end}}
<a class="{{if .PageIsSettingsPackages}}active{{end}} item" href="{{.OrgLink}}/settings/packages">
{{.locale.Tr "packages.title"}}
</a>
<a class="{{if .PageIsSettingsDelete}}active{{end}} item" href="{{.OrgLink}}/settings/delete">
{{.locale.Tr "org.settings.delete"}}
</a>

View file

@ -0,0 +1,14 @@
{{template "base/head" .}}
<div class="page-content organization settings packages">
{{template "org/header" .}}
<div class="ui container">
<div class="ui grid">
{{template "org/settings/navbar" .}}
<div class="twelve wide column content">
{{template "base/alert" .}}
{{template "package/shared/cleanup_rules/list" .}}
</div>
</div>
</div>
</div>
{{template "base/footer" .}}

View file

@ -0,0 +1,14 @@
{{template "base/head" .}}
<div class="page-content organization settings packages">
{{template "org/header" .}}
<div class="ui container">
<div class="ui grid">
{{template "org/settings/navbar" .}}
<div class="twelve wide column content">
{{template "base/alert" .}}
{{template "package/shared/cleanup_rules/edit" .}}
</div>
</div>
</div>
</div>
{{template "base/footer" .}}

View file

@ -0,0 +1,13 @@
{{template "base/head" .}}
<div class="page-content organization settings packages admin">
{{template "org/header" .}}
<div class="ui container">
<div class="ui grid">
{{template "org/settings/navbar" .}}
<div class="twelve wide column content">
{{template "package/shared/cleanup_rules/preview" .}}
</div>
</div>
</div>
</div>
{{template "base/footer" .}}

View file

@ -0,0 +1,73 @@
<h4 class="ui top attached header">{{if .IsEditRule}}{{.locale.Tr "packages.owner.settings.cleanuprules.edit"}}{{else}}{{.locale.Tr "packages.owner.settings.cleanuprules.add"}}{{end}}</h4>
<div class="ui attached segment">
<form class="ui form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<input name="id" type="hidden" value="{{.CleanupRule.ID}}">
<div class="field">
<div class="ui checkbox">
<label>{{.locale.Tr "enabled"}}</label>
<input type="checkbox" name="enabled" {{if .CleanupRule.Enabled}}checked{{end}}>
</div>
</div>
<div class="{{if .IsEditRule}}disabled {{end}}field {{if .Err_Type}}error{{end}}">
<label>{{.locale.Tr "packages.filter.type"}}</label>
<select class="ui selection dropdown" name="type">
{{range $type := .AvailableTypes}}
<option{{if eq $.CleanupRule.Type $type}} selected="selected"{{end}} value="{{$type}}">{{$type.Name}}</option>
{{end}}
</select>
</div>
<div class="field">
<div class="ui checkbox">
<label>{{.locale.Tr "packages.owner.settings.cleanuprules.pattern_full_match"}}</label>
<input type="checkbox" name="match_full_name" {{if .CleanupRule.MatchFullName}}checked{{end}}>
</div>
</div>
<div class="ui divider"></div>
<p>{{.locale.Tr "packages.owner.settings.cleanuprules.keep.title"}}</p>
<div class="field {{if .Err_KeepCount}}error{{end}}">
<label>{{.locale.Tr "packages.owner.settings.cleanuprules.keep.count"}}:</label>
<select class="ui selection dropdown" name="keep_count">
<option{{if eq .CleanupRule.KeepCount 0}} selected="selected"{{end}} value="0"></option>
<option{{if eq .CleanupRule.KeepCount 1}} selected="selected"{{end}} value="1">{{.locale.Tr "packages.owner.settings.cleanuprules.keep.count.1"}}</option>
<option{{if eq .CleanupRule.KeepCount 5}} selected="selected"{{end}} value="5">{{.locale.Tr "packages.owner.settings.cleanuprules.keep.count.n" 5}}</option>
<option{{if eq .CleanupRule.KeepCount 10}} selected="selected"{{end}} value="10">{{.locale.Tr "packages.owner.settings.cleanuprules.keep.count.n" 10}}</option>
<option{{if eq .CleanupRule.KeepCount 25}} selected="selected"{{end}} value="25">{{.locale.Tr "packages.owner.settings.cleanuprules.keep.count.n" 25}}</option>
<option{{if eq .CleanupRule.KeepCount 50}} selected="selected"{{end}} value="50">{{.locale.Tr "packages.owner.settings.cleanuprules.keep.count.n" 50}}</option>
<option{{if eq .CleanupRule.KeepCount 100}} selected="selected"{{end}} value="100">{{.locale.Tr "packages.owner.settings.cleanuprules.keep.count.n" 100}}</option>
</select>
</div>
<div class="field {{if .Err_KeepPattern}}error{{end}}">
<label>{{.locale.Tr "packages.owner.settings.cleanuprules.keep.pattern"}}:</label>
<input name="keep_pattern" type="text" value="{{.CleanupRule.KeepPattern}}">
<p>{{.locale.Tr "packages.owner.settings.cleanuprules.keep.pattern.container" | Safe}}</p>
</div>
<div class="ui divider"></div>
<p>{{.locale.Tr "packages.owner.settings.cleanuprules.remove.title"}}</p>
<div class="field {{if .Err_RemoveDays}}error{{end}}">
<label>{{.locale.Tr "packages.owner.settings.cleanuprules.remove.days"}}:</label>
<select class="ui selection dropdown" name="remove_days">
<option{{if eq .CleanupRule.RemoveDays 0}} selected="selected"{{end}} value="0"></option>
<option{{if eq .CleanupRule.RemoveDays 7}} selected="selected"{{end}} value="7">{{.locale.Tr "tool.days" 7}}</option>
<option{{if eq .CleanupRule.RemoveDays 14}} selected="selected"{{end}} value="14">{{.locale.Tr "tool.days" 14}}</option>
<option{{if eq .CleanupRule.RemoveDays 30}} selected="selected"{{end}} value="30">{{.locale.Tr "tool.days" 30}}</option>
<option{{if eq .CleanupRule.RemoveDays 60}} selected="selected"{{end}} value="60">{{.locale.Tr "tool.days" 60}}</option>
<option{{if eq .CleanupRule.RemoveDays 90}} selected="selected"{{end}} value="90">{{.locale.Tr "tool.days" 90}}</option>
<option{{if eq .CleanupRule.RemoveDays 180}} selected="selected"{{end}} value="180">{{.locale.Tr "tool.days" 180}}</option>
</select>
</div>
<div class="field {{if .Err_RemovePattern}}error{{end}}">
<label>{{.locale.Tr "packages.owner.settings.cleanuprules.remove.pattern"}}:</label>
<input name="remove_pattern" type="text" value="{{.CleanupRule.RemovePattern}}">
</div>
<div class="field">
{{if .IsEditRule}}
<button class="ui green button" name="action" value="save">{{.locale.Tr "save"}}</button>
<button class="ui red button" name="action" value="remove">{{.locale.Tr "remove"}}</button>
<a class="ui button" href="{{.Link}}/preview">{{.locale.Tr "packages.owner.settings.cleanuprules.preview"}}</a>
{{else}}
<button class="ui green button" name="action" value="save">{{.locale.Tr "add"}}</button>
{{end}}
</div>
</form>
</div>

View file

@ -0,0 +1,34 @@
<h4 class="ui top attached header">
{{.locale.Tr "packages.owner.settings.cleanuprules.title"}}
<div class="ui right">
<a class="ui primary tiny button" href="{{.Link}}/rules/add">{{.locale.Tr "packages.owner.settings.cleanuprules.add"}}</a>
</div>
</h4>
<div class="ui attached segment">
<div class="ui key list">
{{range .CleanupRules}}
<div class="item">
<div class="right floated content">
<div class="ui dropdown tiny basic button icon-button">
{{svg "octicon-kebab-horizontal"}}
<div class="menu">
<a class="item" href="{{$.Link}}/rules/{{.ID}}">{{$.locale.Tr "edit"}}</a>
<a class="item" href="{{$.Link}}/rules/{{.ID}}/preview">{{$.locale.Tr "packages.owner.settings.cleanuprules.preview"}}</a>
</div>
</div>
</div>
<i class="icon">{{svg .Type.SVGName 36}}</i>
<div class="content">
<a class="item" href="{{$.Link}}/rules/{{.ID}}"><strong>{{.Type.Name}}</strong></a>
<div><i>{{if .Enabled}}{{$.locale.Tr "enabled"}}{{else}}{{$.locale.Tr "disabled"}}{{end}}</i></div>
{{if .KeepCount}}<div><i>{{$.locale.Tr "packages.owner.settings.cleanuprules.keep.count"}}:</i> {{if eq .KeepCount 1}}{{$.locale.Tr "packages.owner.settings.cleanuprules.keep.count.1"}}{{else}}{{$.locale.Tr "packages.owner.settings.cleanuprules.keep.count.n" .KeepCount}}{{end}}</div>{{end}}
{{if .KeepPattern}}<div><i>{{$.locale.Tr "packages.owner.settings.cleanuprules.keep.pattern"}}:</i> {{EllipsisString .KeepPattern 100}}</div>{{end}}
{{if .RemoveDays}}<div><i>{{$.locale.Tr "packages.owner.settings.cleanuprules.remove.days"}}:</i> {{$.locale.Tr "tool.days" .RemoveDays}}</div>{{end}}
{{if .RemovePattern}}<div><i>{{$.locale.Tr "packages.owner.settings.cleanuprules.remove.pattern"}}:</i> {{EllipsisString .RemovePattern 100}}</div>{{end}}
</div>
</div>
{{else}}
<div class="item">{{.locale.Tr "packages.owner.settings.cleanuprules.none"}}</div>
{{end}}
</div>
</div>

View file

@ -0,0 +1,34 @@
<h4 class="ui top attached header">{{.locale.Tr "packages.owner.settings.cleanuprules.preview"}}</h4>
<div class="ui attached segment">
<p>{{.locale.Tr "packages.owner.settings.cleanuprules.preview.overview" (len .VersionsToRemove)}}</p>
</div>
<div class="ui attached table segment">
<table class="ui very basic striped table unstackable">
<thead>
<tr>
<th>{{.locale.Tr "admin.packages.type"}}</th>
<th>{{.locale.Tr "admin.packages.name"}}</th>
<th>{{.locale.Tr "admin.packages.version"}}</th>
<th>{{.locale.Tr "admin.packages.creator"}}</th>
<th>{{.locale.Tr "admin.packages.size"}}</th>
<th>{{.locale.Tr "admin.packages.published"}}</th>
</tr>
</thead>
<tbody>
{{range .VersionsToRemove}}
<tr>
<td>{{.Package.Type.Name}}</td>
<td>{{.Package.Name}}</td>
<td><a href="{{.FullWebLink}}">{{.Version.Version}}</a></td>
<td><a href="{{.Creator.HomeLink}}">{{.Creator.Name}}</a></td>
<td>{{FileSize .CalculateBlobSize}}</td>
<td><span title="{{.Version.CreatedUnix.FormatLong}}"><time data-format="short-date" datetime="{{.Version.CreatedUnix.FormatLong}}">{{.Version.CreatedUnix.FormatShort}}</time></span></td>
</tr>
{{else}}
<tr>
<td colspan="6">{{.locale.Tr "packages.owner.settings.cleanuprules.preview.none"}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>

View file

@ -18,6 +18,9 @@
<a class="{{if .PageIsSettingsKeys}}active{{end}} item" href="{{AppSubUrl}}/user/settings/keys">
{{.locale.Tr "settings.ssh_gpg_keys"}}
</a>
<a class="{{if .PageIsSettingsPackages}}active{{end}} item" href="{{AppSubUrl}}/user/settings/packages">
{{.locale.Tr "packages.title"}}
</a>
<a class="{{if .PageIsSettingsOrganization}}active{{end}} item" href="{{AppSubUrl}}/user/settings/organization">
{{.locale.Tr "settings.organization"}}
</a>

View file

@ -0,0 +1,9 @@
{{template "base/head" .}}
<div class="page-content user settings packages">
{{template "user/settings/navbar" .}}
<div class="ui container">
{{template "base/alert" .}}
{{template "package/shared/cleanup_rules/list" .}}
</div>
</div>
{{template "base/footer" .}}

View file

@ -0,0 +1,9 @@
{{template "base/head" .}}
<div class="page-content user settings packages">
{{template "user/settings/navbar" .}}
<div class="ui container">
{{template "base/alert" .}}
{{template "package/shared/cleanup_rules/edit" .}}
</div>
</div>
{{template "base/footer" .}}

View file

@ -0,0 +1,8 @@
{{template "base/head" .}}
<div class="page-content user settings packages admin">
{{template "user/settings/navbar" .}}
<div class="ui container">
{{template "package/shared/cleanup_rules/preview" .}}
</div>
</div>
{{template "base/footer" .}}