Add package cleanup rules.
This commit is contained in:
parent
9a70a12a34
commit
1c160956da
23 changed files with 920 additions and 1 deletions
|
@ -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
29
models/migrations/v232.go
Normal 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))
|
||||
}
|
|
@ -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 {
|
||||
|
|
111
models/packages/package_cleanup_rule.go
Normal file
111
models/packages/package_cleanup_rule.go
Normal 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,
|
||||
)
|
||||
}
|
|
@ -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.
|
||||
|
|
87
routers/web/org/setting_packages.go
Normal file
87
routers/web/org/setting_packages.go
Normal 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)
|
||||
}
|
232
routers/web/shared/packages/packages.go
Normal file
232
routers/web/shared/packages/packages.go
Normal 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
|
||||
}
|
80
routers/web/user/setting/packages.go
Normal file
80
routers/web/user/setting/packages.go
Normal 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)
|
||||
}
|
|
@ -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
|
||||
})
|
||||
|
|
31
services/forms/package_form.go
Normal file
31
services/forms/package_form.go
Normal 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)
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
14
templates/org/settings/packages.tmpl
Normal file
14
templates/org/settings/packages.tmpl
Normal 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" .}}
|
14
templates/org/settings/packages_cleanup_rules_edit.tmpl
Normal file
14
templates/org/settings/packages_cleanup_rules_edit.tmpl
Normal 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" .}}
|
13
templates/org/settings/packages_cleanup_rules_preview.tmpl
Normal file
13
templates/org/settings/packages_cleanup_rules_preview.tmpl
Normal 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" .}}
|
73
templates/package/shared/cleanup_rules/edit.tmpl
Normal file
73
templates/package/shared/cleanup_rules/edit.tmpl
Normal 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>
|
34
templates/package/shared/cleanup_rules/list.tmpl
Normal file
34
templates/package/shared/cleanup_rules/list.tmpl
Normal 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>
|
34
templates/package/shared/cleanup_rules/preview.tmpl
Normal file
34
templates/package/shared/cleanup_rules/preview.tmpl
Normal 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>
|
|
@ -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>
|
||||
|
|
9
templates/user/settings/packages.tmpl
Normal file
9
templates/user/settings/packages.tmpl
Normal 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" .}}
|
9
templates/user/settings/packages_cleanup_rules_edit.tmpl
Normal file
9
templates/user/settings/packages_cleanup_rules_edit.tmpl
Normal 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" .}}
|
|
@ -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" .}}
|
Loading…
Add table
Reference in a new issue