Merge pull request 'Soft-quota foundations' (#4212) from algernon/forgejo:quota/helpers into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/4212 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
commit
63fdc1298f
61 changed files with 8638 additions and 72 deletions
|
@ -76,6 +76,8 @@ var migrations = []*Migration{
|
||||||
NewMigration("Create the `following_repo` table", CreateFollowingRepoTable),
|
NewMigration("Create the `following_repo` table", CreateFollowingRepoTable),
|
||||||
// v19 -> v20
|
// v19 -> v20
|
||||||
NewMigration("Add external_url to attachment table", AddExternalURLColumnToAttachmentTable),
|
NewMigration("Add external_url to attachment table", AddExternalURLColumnToAttachmentTable),
|
||||||
|
// v20 -> v21
|
||||||
|
NewMigration("Creating Quota-related tables", CreateQuotaTables),
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentDBVersion returns the current Forgejo database version.
|
// GetCurrentDBVersion returns the current Forgejo database version.
|
||||||
|
|
52
models/forgejo_migrations/v20.go
Normal file
52
models/forgejo_migrations/v20.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package forgejo_migrations //nolint:revive
|
||||||
|
|
||||||
|
import "xorm.io/xorm"
|
||||||
|
|
||||||
|
type (
|
||||||
|
QuotaLimitSubject int
|
||||||
|
QuotaLimitSubjects []QuotaLimitSubject
|
||||||
|
|
||||||
|
QuotaKind int
|
||||||
|
)
|
||||||
|
|
||||||
|
type QuotaRule struct {
|
||||||
|
Name string `xorm:"pk not null"`
|
||||||
|
Limit int64 `xorm:"NOT NULL"`
|
||||||
|
Subjects QuotaLimitSubjects
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuotaGroup struct {
|
||||||
|
Name string `xorm:"pk NOT NULL"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuotaGroupRuleMapping struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
GroupName string `xorm:"index unique(qgrm_gr) not null"`
|
||||||
|
RuleName string `xorm:"unique(qgrm_gr) not null"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuotaGroupMapping struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
Kind QuotaKind `xorm:"unique(qgm_kmg) not null"`
|
||||||
|
MappedID int64 `xorm:"unique(qgm_kmg) not null"`
|
||||||
|
GroupName string `xorm:"index unique(qgm_kmg) not null"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateQuotaTables(x *xorm.Engine) error {
|
||||||
|
if err := x.Sync(new(QuotaRule)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := x.Sync(new(QuotaGroup)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := x.Sync(new(QuotaGroupRuleMapping)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.Sync(new(QuotaGroupMapping))
|
||||||
|
}
|
127
models/quota/errors.go
Normal file
127
models/quota/errors.go
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package quota
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type ErrRuleAlreadyExists struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsErrRuleAlreadyExists(err error) bool {
|
||||||
|
_, ok := err.(ErrRuleAlreadyExists)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrRuleAlreadyExists) Error() string {
|
||||||
|
return fmt.Sprintf("rule already exists: [name: %s]", err.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrRuleNotFound struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsErrRuleNotFound(err error) bool {
|
||||||
|
_, ok := err.(ErrRuleNotFound)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrRuleNotFound) Error() string {
|
||||||
|
return fmt.Sprintf("rule not found: [name: %s]", err.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrGroupAlreadyExists struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsErrGroupAlreadyExists(err error) bool {
|
||||||
|
_, ok := err.(ErrGroupAlreadyExists)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrGroupAlreadyExists) Error() string {
|
||||||
|
return fmt.Sprintf("group already exists: [name: %s]", err.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrGroupNotFound struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsErrGroupNotFound(err error) bool {
|
||||||
|
_, ok := err.(ErrGroupNotFound)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrGroupNotFound) Error() string {
|
||||||
|
return fmt.Sprintf("group not found: [group: %s]", err.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrUserAlreadyInGroup struct {
|
||||||
|
GroupName string
|
||||||
|
UserID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsErrUserAlreadyInGroup(err error) bool {
|
||||||
|
_, ok := err.(ErrUserAlreadyInGroup)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrUserAlreadyInGroup) Error() string {
|
||||||
|
return fmt.Sprintf("user already in group: [group: %s, userID: %d]", err.GroupName, err.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrUserNotInGroup struct {
|
||||||
|
GroupName string
|
||||||
|
UserID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsErrUserNotInGroup(err error) bool {
|
||||||
|
_, ok := err.(ErrUserNotInGroup)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrUserNotInGroup) Error() string {
|
||||||
|
return fmt.Sprintf("user not in group: [group: %s, userID: %d]", err.GroupName, err.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrRuleAlreadyInGroup struct {
|
||||||
|
GroupName string
|
||||||
|
RuleName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsErrRuleAlreadyInGroup(err error) bool {
|
||||||
|
_, ok := err.(ErrRuleAlreadyInGroup)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrRuleAlreadyInGroup) Error() string {
|
||||||
|
return fmt.Sprintf("rule already in group: [group: %s, rule: %s]", err.GroupName, err.RuleName)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrRuleNotInGroup struct {
|
||||||
|
GroupName string
|
||||||
|
RuleName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsErrRuleNotInGroup(err error) bool {
|
||||||
|
_, ok := err.(ErrRuleNotInGroup)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrRuleNotInGroup) Error() string {
|
||||||
|
return fmt.Sprintf("rule not in group: [group: %s, rule: %s]", err.GroupName, err.RuleName)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrParseLimitSubjectUnrecognized struct {
|
||||||
|
Subject string
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsErrParseLimitSubjectUnrecognized(err error) bool {
|
||||||
|
_, ok := err.(ErrParseLimitSubjectUnrecognized)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrParseLimitSubjectUnrecognized) Error() string {
|
||||||
|
return fmt.Sprintf("unrecognized quota limit subject: [subject: %s]", err.Subject)
|
||||||
|
}
|
401
models/quota/group.go
Normal file
401
models/quota/group.go
Normal file
|
@ -0,0 +1,401 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package quota
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
GroupList []*Group
|
||||||
|
Group struct {
|
||||||
|
// Name of the quota group
|
||||||
|
Name string `json:"name" xorm:"pk NOT NULL" binding:"Required"`
|
||||||
|
Rules []Rule `json:"rules" xorm:"-"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type GroupRuleMapping struct {
|
||||||
|
ID int64 `xorm:"pk autoincr" json:"-"`
|
||||||
|
GroupName string `xorm:"index unique(qgrm_gr) not null" json:"group_name"`
|
||||||
|
RuleName string `xorm:"unique(qgrm_gr) not null" json:"rule_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Kind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
KindUser Kind = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
type GroupMapping struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
Kind Kind `xorm:"unique(qgm_kmg) not null"`
|
||||||
|
MappedID int64 `xorm:"unique(qgm_kmg) not null"`
|
||||||
|
GroupName string `xorm:"index unique(qgm_kmg) not null"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) TableName() string {
|
||||||
|
return "quota_group"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (grm *GroupRuleMapping) TableName() string {
|
||||||
|
return "quota_group_rule_mapping"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ugm *GroupMapping) TableName() string {
|
||||||
|
return "quota_group_mapping"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) LoadRules(ctx context.Context) error {
|
||||||
|
return db.GetEngine(ctx).Select("`quota_rule`.*").
|
||||||
|
Table("quota_rule").
|
||||||
|
Join("INNER", "`quota_group_rule_mapping`", "`quota_group_rule_mapping`.rule_name = `quota_rule`.name").
|
||||||
|
Where("`quota_group_rule_mapping`.group_name = ?", g.Name).
|
||||||
|
Find(&g.Rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) isUserInGroup(ctx context.Context, userID int64) (bool, error) {
|
||||||
|
return db.GetEngine(ctx).
|
||||||
|
Where("kind = ? AND mapped_id = ? AND group_name = ?", KindUser, userID, g.Name).
|
||||||
|
Get(&GroupMapping{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) AddUserByID(ctx context.Context, userID int64) error {
|
||||||
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer committer.Close()
|
||||||
|
|
||||||
|
exists, err := g.isUserInGroup(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if exists {
|
||||||
|
return ErrUserAlreadyInGroup{GroupName: g.Name, UserID: userID}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.GetEngine(ctx).Insert(&GroupMapping{
|
||||||
|
Kind: KindUser,
|
||||||
|
MappedID: userID,
|
||||||
|
GroupName: g.Name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return committer.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) RemoveUserByID(ctx context.Context, userID int64) error {
|
||||||
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer committer.Close()
|
||||||
|
|
||||||
|
exists, err := g.isUserInGroup(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !exists {
|
||||||
|
return ErrUserNotInGroup{GroupName: g.Name, UserID: userID}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.GetEngine(ctx).Delete(&GroupMapping{
|
||||||
|
Kind: KindUser,
|
||||||
|
MappedID: userID,
|
||||||
|
GroupName: g.Name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return committer.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) isRuleInGroup(ctx context.Context, ruleName string) (bool, error) {
|
||||||
|
return db.GetEngine(ctx).
|
||||||
|
Where("group_name = ? AND rule_name = ?", g.Name, ruleName).
|
||||||
|
Get(&GroupRuleMapping{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) AddRuleByName(ctx context.Context, ruleName string) error {
|
||||||
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer committer.Close()
|
||||||
|
|
||||||
|
exists, err := DoesRuleExist(ctx, ruleName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !exists {
|
||||||
|
return ErrRuleNotFound{Name: ruleName}
|
||||||
|
}
|
||||||
|
|
||||||
|
has, err := g.isRuleInGroup(ctx, ruleName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if has {
|
||||||
|
return ErrRuleAlreadyInGroup{GroupName: g.Name, RuleName: ruleName}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.GetEngine(ctx).Insert(&GroupRuleMapping{
|
||||||
|
GroupName: g.Name,
|
||||||
|
RuleName: ruleName,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return committer.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) RemoveRuleByName(ctx context.Context, ruleName string) error {
|
||||||
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer committer.Close()
|
||||||
|
|
||||||
|
exists, err := g.isRuleInGroup(ctx, ruleName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !exists {
|
||||||
|
return ErrRuleNotInGroup{GroupName: g.Name, RuleName: ruleName}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.GetEngine(ctx).Delete(&GroupRuleMapping{
|
||||||
|
GroupName: g.Name,
|
||||||
|
RuleName: ruleName,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return committer.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
var affectsMap = map[LimitSubject]LimitSubjects{
|
||||||
|
LimitSubjectSizeAll: {
|
||||||
|
LimitSubjectSizeReposAll,
|
||||||
|
LimitSubjectSizeGitLFS,
|
||||||
|
LimitSubjectSizeAssetsAll,
|
||||||
|
},
|
||||||
|
LimitSubjectSizeReposAll: {
|
||||||
|
LimitSubjectSizeReposPublic,
|
||||||
|
LimitSubjectSizeReposPrivate,
|
||||||
|
},
|
||||||
|
LimitSubjectSizeAssetsAll: {
|
||||||
|
LimitSubjectSizeAssetsAttachmentsAll,
|
||||||
|
LimitSubjectSizeAssetsArtifacts,
|
||||||
|
LimitSubjectSizeAssetsPackagesAll,
|
||||||
|
},
|
||||||
|
LimitSubjectSizeAssetsAttachmentsAll: {
|
||||||
|
LimitSubjectSizeAssetsAttachmentsIssues,
|
||||||
|
LimitSubjectSizeAssetsAttachmentsReleases,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) Evaluate(used Used, forSubject LimitSubject) (bool, bool) {
|
||||||
|
var found bool
|
||||||
|
for _, rule := range g.Rules {
|
||||||
|
ok, has := rule.Evaluate(used, forSubject)
|
||||||
|
if has {
|
||||||
|
found = true
|
||||||
|
if !ok {
|
||||||
|
return false, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
// If Evaluation for forSubject did not succeed, try evaluating against
|
||||||
|
// subjects below
|
||||||
|
|
||||||
|
for _, subject := range affectsMap[forSubject] {
|
||||||
|
ok, has := g.Evaluate(used, subject)
|
||||||
|
if has {
|
||||||
|
found = true
|
||||||
|
if !ok {
|
||||||
|
return false, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, found
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gl *GroupList) Evaluate(used Used, forSubject LimitSubject) bool {
|
||||||
|
// If there are no groups, default to success:
|
||||||
|
if gl == nil || len(*gl) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range *gl {
|
||||||
|
ok, has := group.Evaluate(used, forSubject)
|
||||||
|
if has && ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetGroupByName(ctx context.Context, name string) (*Group, error) {
|
||||||
|
var group Group
|
||||||
|
has, err := db.GetEngine(ctx).Where("name = ?", name).Get(&group)
|
||||||
|
if has {
|
||||||
|
if err = group.LoadRules(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &group, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListGroups(ctx context.Context) (GroupList, error) {
|
||||||
|
var groups GroupList
|
||||||
|
err := db.GetEngine(ctx).Find(&groups)
|
||||||
|
return groups, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func doesGroupExist(ctx context.Context, name string) (bool, error) {
|
||||||
|
return db.GetEngine(ctx).Where("name = ?", name).Get(&Group{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateGroup(ctx context.Context, name string) (*Group, error) {
|
||||||
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer committer.Close()
|
||||||
|
|
||||||
|
exists, err := doesGroupExist(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if exists {
|
||||||
|
return nil, ErrGroupAlreadyExists{Name: name}
|
||||||
|
}
|
||||||
|
|
||||||
|
group := Group{Name: name}
|
||||||
|
_, err = db.GetEngine(ctx).Insert(group)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &group, committer.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListUsersInGroup(ctx context.Context, name string) ([]*user_model.User, error) {
|
||||||
|
group, err := GetGroupByName(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var users []*user_model.User
|
||||||
|
err = db.GetEngine(ctx).Select("`user`.*").
|
||||||
|
Table("user").
|
||||||
|
Join("INNER", "`quota_group_mapping`", "`quota_group_mapping`.mapped_id = `user`.id").
|
||||||
|
Where("`quota_group_mapping`.kind = ? AND `quota_group_mapping`.group_name = ?", KindUser, group.Name).
|
||||||
|
Find(&users)
|
||||||
|
return users, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteGroupByName(ctx context.Context, name string) error {
|
||||||
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer committer.Close()
|
||||||
|
|
||||||
|
_, err = db.GetEngine(ctx).Delete(GroupMapping{
|
||||||
|
GroupName: name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = db.GetEngine(ctx).Delete(GroupRuleMapping{
|
||||||
|
GroupName: name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.GetEngine(ctx).Delete(Group{Name: name})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return committer.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetUserGroups(ctx context.Context, userID int64, groups *[]string) error {
|
||||||
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer committer.Close()
|
||||||
|
|
||||||
|
// First: remove the user from any groups
|
||||||
|
_, err = db.GetEngine(ctx).Where("kind = ? AND mapped_id = ?", KindUser, userID).Delete(GroupMapping{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if groups == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then add the user to each group listed
|
||||||
|
for _, groupName := range *groups {
|
||||||
|
group, err := GetGroupByName(ctx, groupName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if group == nil {
|
||||||
|
return ErrGroupNotFound{Name: groupName}
|
||||||
|
}
|
||||||
|
err = group.AddUserByID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return committer.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetGroupsForUser(ctx context.Context, userID int64) (GroupList, error) {
|
||||||
|
var groups GroupList
|
||||||
|
err := db.GetEngine(ctx).
|
||||||
|
Where(builder.In("name",
|
||||||
|
builder.Select("group_name").
|
||||||
|
From("quota_group_mapping").
|
||||||
|
Where(builder.And(
|
||||||
|
builder.Eq{"kind": KindUser},
|
||||||
|
builder.Eq{"mapped_id": userID}),
|
||||||
|
))).
|
||||||
|
Find(&groups)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(groups) == 0 {
|
||||||
|
err = db.GetEngine(ctx).Where(builder.In("name", setting.Quota.DefaultGroups)).Find(&groups)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(groups) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range groups {
|
||||||
|
err = group.LoadRules(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups, nil
|
||||||
|
}
|
69
models/quota/limit_subject.go
Normal file
69
models/quota/limit_subject.go
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package quota
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type (
|
||||||
|
LimitSubject int
|
||||||
|
LimitSubjects []LimitSubject
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LimitSubjectNone LimitSubject = iota
|
||||||
|
LimitSubjectSizeAll
|
||||||
|
LimitSubjectSizeReposAll
|
||||||
|
LimitSubjectSizeReposPublic
|
||||||
|
LimitSubjectSizeReposPrivate
|
||||||
|
LimitSubjectSizeGitAll
|
||||||
|
LimitSubjectSizeGitLFS
|
||||||
|
LimitSubjectSizeAssetsAll
|
||||||
|
LimitSubjectSizeAssetsAttachmentsAll
|
||||||
|
LimitSubjectSizeAssetsAttachmentsIssues
|
||||||
|
LimitSubjectSizeAssetsAttachmentsReleases
|
||||||
|
LimitSubjectSizeAssetsArtifacts
|
||||||
|
LimitSubjectSizeAssetsPackagesAll
|
||||||
|
LimitSubjectSizeWiki
|
||||||
|
|
||||||
|
LimitSubjectFirst = LimitSubjectSizeAll
|
||||||
|
LimitSubjectLast = LimitSubjectSizeWiki
|
||||||
|
)
|
||||||
|
|
||||||
|
var limitSubjectRepr = map[string]LimitSubject{
|
||||||
|
"none": LimitSubjectNone,
|
||||||
|
"size:all": LimitSubjectSizeAll,
|
||||||
|
"size:repos:all": LimitSubjectSizeReposAll,
|
||||||
|
"size:repos:public": LimitSubjectSizeReposPublic,
|
||||||
|
"size:repos:private": LimitSubjectSizeReposPrivate,
|
||||||
|
"size:git:all": LimitSubjectSizeGitAll,
|
||||||
|
"size:git:lfs": LimitSubjectSizeGitLFS,
|
||||||
|
"size:assets:all": LimitSubjectSizeAssetsAll,
|
||||||
|
"size:assets:attachments:all": LimitSubjectSizeAssetsAttachmentsAll,
|
||||||
|
"size:assets:attachments:issues": LimitSubjectSizeAssetsAttachmentsIssues,
|
||||||
|
"size:assets:attachments:releases": LimitSubjectSizeAssetsAttachmentsReleases,
|
||||||
|
"size:assets:artifacts": LimitSubjectSizeAssetsArtifacts,
|
||||||
|
"size:assets:packages:all": LimitSubjectSizeAssetsPackagesAll,
|
||||||
|
"size:assets:wiki": LimitSubjectSizeWiki,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (subject LimitSubject) String() string {
|
||||||
|
for repr, limit := range limitSubjectRepr {
|
||||||
|
if limit == subject {
|
||||||
|
return repr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "<unknown>"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (subjects LimitSubjects) GoString() string {
|
||||||
|
return fmt.Sprintf("%T{%+v}", subjects, subjects)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseLimitSubject(repr string) (LimitSubject, error) {
|
||||||
|
result, has := limitSubjectRepr[repr]
|
||||||
|
if !has {
|
||||||
|
return LimitSubjectNone, ErrParseLimitSubjectUnrecognized{Subject: repr}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
36
models/quota/quota.go
Normal file
36
models/quota/quota.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package quota
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
db.RegisterModel(new(Rule))
|
||||||
|
db.RegisterModel(new(Group))
|
||||||
|
db.RegisterModel(new(GroupRuleMapping))
|
||||||
|
db.RegisterModel(new(GroupMapping))
|
||||||
|
}
|
||||||
|
|
||||||
|
func EvaluateForUser(ctx context.Context, userID int64, subject LimitSubject) (bool, error) {
|
||||||
|
if !setting.Quota.Enabled {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
groups, err := GetGroupsForUser(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
used, err := GetUsedForUser(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups.Evaluate(*used, subject), nil
|
||||||
|
}
|
208
models/quota/quota_group_test.go
Normal file
208
models/quota/quota_group_test.go
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package quota_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestQuotaGroupAllRulesMustPass(t *testing.T) {
|
||||||
|
unlimitedRule := quota_model.Rule{
|
||||||
|
Limit: -1,
|
||||||
|
Subjects: quota_model.LimitSubjects{
|
||||||
|
quota_model.LimitSubjectSizeAll,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
denyRule := quota_model.Rule{
|
||||||
|
Limit: 0,
|
||||||
|
Subjects: quota_model.LimitSubjects{
|
||||||
|
quota_model.LimitSubjectSizeAll,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
group := quota_model.Group{
|
||||||
|
Rules: []quota_model.Rule{
|
||||||
|
unlimitedRule,
|
||||||
|
denyRule,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
used := quota_model.Used{}
|
||||||
|
used.Size.Repos.Public = 1024
|
||||||
|
|
||||||
|
// Within a group, *all* rules must pass. Thus, if we have a deny-all rule,
|
||||||
|
// and an unlimited rule, that will always fail.
|
||||||
|
ok, has := group.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||||
|
assert.True(t, has)
|
||||||
|
assert.False(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuotaGroupRuleScenario1(t *testing.T) {
|
||||||
|
group := quota_model.Group{
|
||||||
|
Rules: []quota_model.Rule{
|
||||||
|
{
|
||||||
|
Limit: 1024,
|
||||||
|
Subjects: quota_model.LimitSubjects{
|
||||||
|
quota_model.LimitSubjectSizeAssetsAttachmentsReleases,
|
||||||
|
quota_model.LimitSubjectSizeGitLFS,
|
||||||
|
quota_model.LimitSubjectSizeAssetsPackagesAll,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Limit: 0,
|
||||||
|
Subjects: quota_model.LimitSubjects{
|
||||||
|
quota_model.LimitSubjectSizeGitLFS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
used := quota_model.Used{}
|
||||||
|
used.Size.Assets.Attachments.Releases = 512
|
||||||
|
used.Size.Assets.Packages.All = 256
|
||||||
|
used.Size.Git.LFS = 16
|
||||||
|
|
||||||
|
ok, has := group.Evaluate(used, quota_model.LimitSubjectSizeAssetsAttachmentsReleases)
|
||||||
|
assert.True(t, has, "size:assets:attachments:releases is covered")
|
||||||
|
assert.True(t, ok, "size:assets:attachments:releases passes")
|
||||||
|
|
||||||
|
ok, has = group.Evaluate(used, quota_model.LimitSubjectSizeAssetsPackagesAll)
|
||||||
|
assert.True(t, has, "size:assets:packages:all is covered")
|
||||||
|
assert.True(t, ok, "size:assets:packages:all passes")
|
||||||
|
|
||||||
|
ok, has = group.Evaluate(used, quota_model.LimitSubjectSizeGitLFS)
|
||||||
|
assert.True(t, has, "size:git:lfs is covered")
|
||||||
|
assert.False(t, ok, "size:git:lfs fails")
|
||||||
|
|
||||||
|
ok, has = group.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||||
|
assert.True(t, has, "size:all is covered")
|
||||||
|
assert.False(t, ok, "size:all fails")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuotaGroupRuleCombination(t *testing.T) {
|
||||||
|
repoRule := quota_model.Rule{
|
||||||
|
Limit: 4096,
|
||||||
|
Subjects: quota_model.LimitSubjects{
|
||||||
|
quota_model.LimitSubjectSizeReposAll,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
packagesRule := quota_model.Rule{
|
||||||
|
Limit: 0,
|
||||||
|
Subjects: quota_model.LimitSubjects{
|
||||||
|
quota_model.LimitSubjectSizeAssetsPackagesAll,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
used := quota_model.Used{}
|
||||||
|
used.Size.Repos.Public = 1024
|
||||||
|
used.Size.Assets.Packages.All = 1024
|
||||||
|
|
||||||
|
group := quota_model.Group{
|
||||||
|
Rules: []quota_model.Rule{
|
||||||
|
repoRule,
|
||||||
|
packagesRule,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Git LFS isn't covered by any rule
|
||||||
|
_, has := group.Evaluate(used, quota_model.LimitSubjectSizeGitLFS)
|
||||||
|
assert.False(t, has)
|
||||||
|
|
||||||
|
// repos:all is covered, and is passing
|
||||||
|
ok, has := group.Evaluate(used, quota_model.LimitSubjectSizeReposAll)
|
||||||
|
assert.True(t, has)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
// packages:all is covered, and is failing
|
||||||
|
ok, has = group.Evaluate(used, quota_model.LimitSubjectSizeAssetsPackagesAll)
|
||||||
|
assert.True(t, has)
|
||||||
|
assert.False(t, ok)
|
||||||
|
|
||||||
|
// size:all is covered, and is failing (due to packages:all being over quota)
|
||||||
|
ok, has = group.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||||
|
assert.True(t, has, "size:all should be covered")
|
||||||
|
assert.False(t, ok, "size:all should fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuotaGroupListsRequireOnlyOnePassing(t *testing.T) {
|
||||||
|
unlimitedRule := quota_model.Rule{
|
||||||
|
Limit: -1,
|
||||||
|
Subjects: quota_model.LimitSubjects{
|
||||||
|
quota_model.LimitSubjectSizeAll,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
denyRule := quota_model.Rule{
|
||||||
|
Limit: 0,
|
||||||
|
Subjects: quota_model.LimitSubjects{
|
||||||
|
quota_model.LimitSubjectSizeAll,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
denyGroup := quota_model.Group{
|
||||||
|
Rules: []quota_model.Rule{
|
||||||
|
denyRule,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
unlimitedGroup := quota_model.Group{
|
||||||
|
Rules: []quota_model.Rule{
|
||||||
|
unlimitedRule,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := quota_model.GroupList{&denyGroup, &unlimitedGroup}
|
||||||
|
|
||||||
|
used := quota_model.Used{}
|
||||||
|
used.Size.Repos.Public = 1024
|
||||||
|
|
||||||
|
// In a group list, if any group passes, the entire evaluation passes.
|
||||||
|
ok := groups.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||||
|
assert.True(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuotaGroupListAllFailing(t *testing.T) {
|
||||||
|
denyRule := quota_model.Rule{
|
||||||
|
Limit: 0,
|
||||||
|
Subjects: quota_model.LimitSubjects{
|
||||||
|
quota_model.LimitSubjectSizeAll,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
limitedRule := quota_model.Rule{
|
||||||
|
Limit: 1024,
|
||||||
|
Subjects: quota_model.LimitSubjects{
|
||||||
|
quota_model.LimitSubjectSizeAll,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
denyGroup := quota_model.Group{
|
||||||
|
Rules: []quota_model.Rule{
|
||||||
|
denyRule,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
limitedGroup := quota_model.Group{
|
||||||
|
Rules: []quota_model.Rule{
|
||||||
|
limitedRule,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := quota_model.GroupList{&denyGroup, &limitedGroup}
|
||||||
|
|
||||||
|
used := quota_model.Used{}
|
||||||
|
used.Size.Repos.Public = 2048
|
||||||
|
|
||||||
|
ok := groups.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||||
|
assert.False(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuotaGroupListEmpty(t *testing.T) {
|
||||||
|
groups := quota_model.GroupList{}
|
||||||
|
|
||||||
|
used := quota_model.Used{}
|
||||||
|
used.Size.Repos.Public = 2048
|
||||||
|
|
||||||
|
ok := groups.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||||
|
assert.True(t, ok)
|
||||||
|
}
|
304
models/quota/quota_rule_test.go
Normal file
304
models/quota/quota_rule_test.go
Normal file
|
@ -0,0 +1,304 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package quota_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeFullyUsed() quota_model.Used {
|
||||||
|
return quota_model.Used{
|
||||||
|
Size: quota_model.UsedSize{
|
||||||
|
Repos: quota_model.UsedSizeRepos{
|
||||||
|
Public: 1024,
|
||||||
|
Private: 1024,
|
||||||
|
},
|
||||||
|
Git: quota_model.UsedSizeGit{
|
||||||
|
LFS: 1024,
|
||||||
|
},
|
||||||
|
Assets: quota_model.UsedSizeAssets{
|
||||||
|
Attachments: quota_model.UsedSizeAssetsAttachments{
|
||||||
|
Issues: 1024,
|
||||||
|
Releases: 1024,
|
||||||
|
},
|
||||||
|
Artifacts: 1024,
|
||||||
|
Packages: quota_model.UsedSizeAssetsPackages{
|
||||||
|
All: 1024,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makePartiallyUsed() quota_model.Used {
|
||||||
|
return quota_model.Used{
|
||||||
|
Size: quota_model.UsedSize{
|
||||||
|
Repos: quota_model.UsedSizeRepos{
|
||||||
|
Public: 1024,
|
||||||
|
},
|
||||||
|
Assets: quota_model.UsedSizeAssets{
|
||||||
|
Attachments: quota_model.UsedSizeAssetsAttachments{
|
||||||
|
Releases: 1024,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUsed(used quota_model.Used, subject quota_model.LimitSubject, value int64) *quota_model.Used {
|
||||||
|
switch subject {
|
||||||
|
case quota_model.LimitSubjectSizeReposPublic:
|
||||||
|
used.Size.Repos.Public = value
|
||||||
|
return &used
|
||||||
|
case quota_model.LimitSubjectSizeReposPrivate:
|
||||||
|
used.Size.Repos.Private = value
|
||||||
|
return &used
|
||||||
|
case quota_model.LimitSubjectSizeGitLFS:
|
||||||
|
used.Size.Git.LFS = value
|
||||||
|
return &used
|
||||||
|
case quota_model.LimitSubjectSizeAssetsAttachmentsIssues:
|
||||||
|
used.Size.Assets.Attachments.Issues = value
|
||||||
|
return &used
|
||||||
|
case quota_model.LimitSubjectSizeAssetsAttachmentsReleases:
|
||||||
|
used.Size.Assets.Attachments.Releases = value
|
||||||
|
return &used
|
||||||
|
case quota_model.LimitSubjectSizeAssetsArtifacts:
|
||||||
|
used.Size.Assets.Artifacts = value
|
||||||
|
return &used
|
||||||
|
case quota_model.LimitSubjectSizeAssetsPackagesAll:
|
||||||
|
used.Size.Assets.Packages.All = value
|
||||||
|
return &used
|
||||||
|
case quota_model.LimitSubjectSizeWiki:
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertEvaluation(t *testing.T, rule quota_model.Rule, used quota_model.Used, subject quota_model.LimitSubject, expected bool) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
t.Run(subject.String(), func(t *testing.T) {
|
||||||
|
ok, has := rule.Evaluate(used, subject)
|
||||||
|
assert.True(t, has)
|
||||||
|
assert.Equal(t, expected, ok)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuotaRuleNoEvaluation(t *testing.T) {
|
||||||
|
rule := quota_model.Rule{
|
||||||
|
Limit: 1024,
|
||||||
|
Subjects: quota_model.LimitSubjects{
|
||||||
|
quota_model.LimitSubjectSizeAssetsAttachmentsAll,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
used := quota_model.Used{}
|
||||||
|
used.Size.Repos.Public = 4096
|
||||||
|
|
||||||
|
_, has := rule.Evaluate(used, quota_model.LimitSubjectSizeReposAll)
|
||||||
|
|
||||||
|
// We have a rule for "size:assets:attachments:all", and query for
|
||||||
|
// "size:repos:all". We don't cover that subject, so the evaluation returns
|
||||||
|
// with no rules found.
|
||||||
|
assert.False(t, has)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuotaRuleDirectEvaluation(t *testing.T) {
|
||||||
|
// This function is meant to test direct rule evaluation: cases where we set
|
||||||
|
// a rule for a subject, and we evaluate against the same subject.
|
||||||
|
|
||||||
|
runTest := func(t *testing.T, subject quota_model.LimitSubject, limit, used int64, expected bool) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
rule := quota_model.Rule{
|
||||||
|
Limit: limit,
|
||||||
|
Subjects: quota_model.LimitSubjects{
|
||||||
|
subject,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
usedObj := setUsed(quota_model.Used{}, subject, used)
|
||||||
|
if usedObj == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEvaluation(t, rule, *usedObj, subject, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("limit:0", func(t *testing.T) {
|
||||||
|
// With limit:0, nothing used is fine.
|
||||||
|
t.Run("used:0", func(t *testing.T) {
|
||||||
|
for subject := quota_model.LimitSubjectFirst; subject <= quota_model.LimitSubjectLast; subject++ {
|
||||||
|
runTest(t, subject, 0, 0, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// With limit:0, any usage will fail evaluation
|
||||||
|
t.Run("used:512", func(t *testing.T) {
|
||||||
|
for subject := quota_model.LimitSubjectFirst; subject <= quota_model.LimitSubjectLast; subject++ {
|
||||||
|
runTest(t, subject, 0, 512, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("limit:unlimited", func(t *testing.T) {
|
||||||
|
// With no limits, any usage will succeed evaluation
|
||||||
|
t.Run("used:512", func(t *testing.T) {
|
||||||
|
for subject := quota_model.LimitSubjectFirst; subject <= quota_model.LimitSubjectLast; subject++ {
|
||||||
|
runTest(t, subject, -1, 512, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("limit:1024", func(t *testing.T) {
|
||||||
|
// With a set limit, usage below the limit succeeds
|
||||||
|
t.Run("used:512", func(t *testing.T) {
|
||||||
|
for subject := quota_model.LimitSubjectFirst; subject <= quota_model.LimitSubjectLast; subject++ {
|
||||||
|
runTest(t, subject, 1024, 512, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// With a set limit, usage above the limit fails
|
||||||
|
t.Run("used:2048", func(t *testing.T) {
|
||||||
|
for subject := quota_model.LimitSubjectFirst; subject <= quota_model.LimitSubjectLast; subject++ {
|
||||||
|
runTest(t, subject, 1024, 2048, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuotaRuleCombined(t *testing.T) {
|
||||||
|
rule := quota_model.Rule{
|
||||||
|
Limit: 1024,
|
||||||
|
Subjects: quota_model.LimitSubjects{
|
||||||
|
quota_model.LimitSubjectSizeGitLFS,
|
||||||
|
quota_model.LimitSubjectSizeAssetsAttachmentsReleases,
|
||||||
|
quota_model.LimitSubjectSizeAssetsPackagesAll,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
used := quota_model.Used{
|
||||||
|
Size: quota_model.UsedSize{
|
||||||
|
Repos: quota_model.UsedSizeRepos{
|
||||||
|
Public: 4096,
|
||||||
|
},
|
||||||
|
Git: quota_model.UsedSizeGit{
|
||||||
|
LFS: 256,
|
||||||
|
},
|
||||||
|
Assets: quota_model.UsedSizeAssets{
|
||||||
|
Attachments: quota_model.UsedSizeAssetsAttachments{
|
||||||
|
Issues: 2048,
|
||||||
|
Releases: 256,
|
||||||
|
},
|
||||||
|
Packages: quota_model.UsedSizeAssetsPackages{
|
||||||
|
All: 2560,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expectationMap := map[quota_model.LimitSubject]bool{
|
||||||
|
quota_model.LimitSubjectSizeGitLFS: false,
|
||||||
|
quota_model.LimitSubjectSizeAssetsAttachmentsReleases: false,
|
||||||
|
quota_model.LimitSubjectSizeAssetsPackagesAll: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
for subject := quota_model.LimitSubjectFirst; subject <= quota_model.LimitSubjectLast; subject++ {
|
||||||
|
t.Run(subject.String(), func(t *testing.T) {
|
||||||
|
evalOk, evalHas := rule.Evaluate(used, subject)
|
||||||
|
expected, expectedHas := expectationMap[subject]
|
||||||
|
|
||||||
|
assert.Equal(t, expectedHas, evalHas)
|
||||||
|
if expectedHas {
|
||||||
|
assert.Equal(t, expected, evalOk)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuotaRuleSizeAll(t *testing.T) {
|
||||||
|
runTests := func(t *testing.T, rule quota_model.Rule, expected bool) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
subject := quota_model.LimitSubjectSizeAll
|
||||||
|
|
||||||
|
t.Run("used:0", func(t *testing.T) {
|
||||||
|
used := quota_model.Used{}
|
||||||
|
|
||||||
|
assertEvaluation(t, rule, used, subject, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("used:some-each", func(t *testing.T) {
|
||||||
|
used := makeFullyUsed()
|
||||||
|
|
||||||
|
assertEvaluation(t, rule, used, subject, expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("used:some", func(t *testing.T) {
|
||||||
|
used := makePartiallyUsed()
|
||||||
|
|
||||||
|
assertEvaluation(t, rule, used, subject, expected)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// With all limits set to 0, evaluation always fails if usage > 0
|
||||||
|
t.Run("rule:0", func(t *testing.T) {
|
||||||
|
rule := quota_model.Rule{
|
||||||
|
Limit: 0,
|
||||||
|
Subjects: quota_model.LimitSubjects{
|
||||||
|
quota_model.LimitSubjectSizeAll,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
runTests(t, rule, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// With no limits, evaluation always succeeds
|
||||||
|
t.Run("rule:unlimited", func(t *testing.T) {
|
||||||
|
rule := quota_model.Rule{
|
||||||
|
Limit: -1,
|
||||||
|
Subjects: quota_model.LimitSubjects{
|
||||||
|
quota_model.LimitSubjectSizeAll,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
runTests(t, rule, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// With a specific, very generous limit, evaluation succeeds if the limit isn't exhausted
|
||||||
|
t.Run("rule:generous", func(t *testing.T) {
|
||||||
|
rule := quota_model.Rule{
|
||||||
|
Limit: 102400,
|
||||||
|
Subjects: quota_model.LimitSubjects{
|
||||||
|
quota_model.LimitSubjectSizeAll,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
runTests(t, rule, true)
|
||||||
|
|
||||||
|
t.Run("limit exhaustion", func(t *testing.T) {
|
||||||
|
used := quota_model.Used{
|
||||||
|
Size: quota_model.UsedSize{
|
||||||
|
Repos: quota_model.UsedSizeRepos{
|
||||||
|
Public: 204800,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEvaluation(t, rule, used, quota_model.LimitSubjectSizeAll, false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// With a specific, small limit, evaluation fails
|
||||||
|
t.Run("rule:limited", func(t *testing.T) {
|
||||||
|
rule := quota_model.Rule{
|
||||||
|
Limit: 512,
|
||||||
|
Subjects: quota_model.LimitSubjects{
|
||||||
|
quota_model.LimitSubjectSizeAll,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
runTests(t, rule, false)
|
||||||
|
})
|
||||||
|
}
|
127
models/quota/rule.go
Normal file
127
models/quota/rule.go
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package quota
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Rule struct {
|
||||||
|
Name string `xorm:"pk not null" json:"name,omitempty"`
|
||||||
|
Limit int64 `xorm:"NOT NULL" binding:"Required" json:"limit"`
|
||||||
|
Subjects LimitSubjects `json:"subjects,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Rule) TableName() string {
|
||||||
|
return "quota_rule"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Rule) Evaluate(used Used, forSubject LimitSubject) (bool, bool) {
|
||||||
|
// If there's no limit, short circuit out
|
||||||
|
if r.Limit == -1 {
|
||||||
|
return true, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the rule does not cover forSubject, bail out early
|
||||||
|
if !slices.Contains(r.Subjects, forSubject) {
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var sum int64
|
||||||
|
for _, subject := range r.Subjects {
|
||||||
|
sum += used.CalculateFor(subject)
|
||||||
|
}
|
||||||
|
return sum <= r.Limit, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Rule) Edit(ctx context.Context, limit *int64, subjects *LimitSubjects) (*Rule, error) {
|
||||||
|
cols := []string{}
|
||||||
|
|
||||||
|
if limit != nil {
|
||||||
|
r.Limit = *limit
|
||||||
|
cols = append(cols, "limit")
|
||||||
|
}
|
||||||
|
if subjects != nil {
|
||||||
|
r.Subjects = *subjects
|
||||||
|
cols = append(cols, "subjects")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := db.GetEngine(ctx).Where("name = ?", r.Name).Cols(cols...).Update(r)
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRuleByName(ctx context.Context, name string) (*Rule, error) {
|
||||||
|
var rule Rule
|
||||||
|
has, err := db.GetEngine(ctx).Where("name = ?", name).Get(&rule)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !has {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return &rule, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListRules(ctx context.Context) ([]Rule, error) {
|
||||||
|
var rules []Rule
|
||||||
|
err := db.GetEngine(ctx).Find(&rules)
|
||||||
|
return rules, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func DoesRuleExist(ctx context.Context, name string) (bool, error) {
|
||||||
|
return db.GetEngine(ctx).
|
||||||
|
Where("name = ?", name).
|
||||||
|
Get(&Rule{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateRule(ctx context.Context, name string, limit int64, subjects LimitSubjects) (*Rule, error) {
|
||||||
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer committer.Close()
|
||||||
|
|
||||||
|
exists, err := DoesRuleExist(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if exists {
|
||||||
|
return nil, ErrRuleAlreadyExists{Name: name}
|
||||||
|
}
|
||||||
|
|
||||||
|
rule := Rule{
|
||||||
|
Name: name,
|
||||||
|
Limit: limit,
|
||||||
|
Subjects: subjects,
|
||||||
|
}
|
||||||
|
_, err = db.GetEngine(ctx).Insert(rule)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &rule, committer.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteRuleByName(ctx context.Context, name string) error {
|
||||||
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer committer.Close()
|
||||||
|
|
||||||
|
_, err = db.GetEngine(ctx).Delete(GroupRuleMapping{
|
||||||
|
RuleName: name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.GetEngine(ctx).Delete(Rule{Name: name})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return committer.Commit()
|
||||||
|
}
|
252
models/quota/used.go
Normal file
252
models/quota/used.go
Normal file
|
@ -0,0 +1,252 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package quota
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
action_model "code.gitea.io/gitea/models/actions"
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
package_model "code.gitea.io/gitea/models/packages"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Used struct {
|
||||||
|
Size UsedSize
|
||||||
|
}
|
||||||
|
|
||||||
|
type UsedSize struct {
|
||||||
|
Repos UsedSizeRepos
|
||||||
|
Git UsedSizeGit
|
||||||
|
Assets UsedSizeAssets
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u UsedSize) All() int64 {
|
||||||
|
return u.Repos.All() + u.Git.All(u.Repos) + u.Assets.All()
|
||||||
|
}
|
||||||
|
|
||||||
|
type UsedSizeRepos struct {
|
||||||
|
Public int64
|
||||||
|
Private int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u UsedSizeRepos) All() int64 {
|
||||||
|
return u.Public + u.Private
|
||||||
|
}
|
||||||
|
|
||||||
|
type UsedSizeGit struct {
|
||||||
|
LFS int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u UsedSizeGit) All(r UsedSizeRepos) int64 {
|
||||||
|
return u.LFS + r.All()
|
||||||
|
}
|
||||||
|
|
||||||
|
type UsedSizeAssets struct {
|
||||||
|
Attachments UsedSizeAssetsAttachments
|
||||||
|
Artifacts int64
|
||||||
|
Packages UsedSizeAssetsPackages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u UsedSizeAssets) All() int64 {
|
||||||
|
return u.Attachments.All() + u.Artifacts + u.Packages.All
|
||||||
|
}
|
||||||
|
|
||||||
|
type UsedSizeAssetsAttachments struct {
|
||||||
|
Issues int64
|
||||||
|
Releases int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u UsedSizeAssetsAttachments) All() int64 {
|
||||||
|
return u.Issues + u.Releases
|
||||||
|
}
|
||||||
|
|
||||||
|
type UsedSizeAssetsPackages struct {
|
||||||
|
All int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u Used) CalculateFor(subject LimitSubject) int64 {
|
||||||
|
switch subject {
|
||||||
|
case LimitSubjectNone:
|
||||||
|
return 0
|
||||||
|
case LimitSubjectSizeAll:
|
||||||
|
return u.Size.All()
|
||||||
|
case LimitSubjectSizeReposAll:
|
||||||
|
return u.Size.Repos.All()
|
||||||
|
case LimitSubjectSizeReposPublic:
|
||||||
|
return u.Size.Repos.Public
|
||||||
|
case LimitSubjectSizeReposPrivate:
|
||||||
|
return u.Size.Repos.Private
|
||||||
|
case LimitSubjectSizeGitAll:
|
||||||
|
return u.Size.Git.All(u.Size.Repos)
|
||||||
|
case LimitSubjectSizeGitLFS:
|
||||||
|
return u.Size.Git.LFS
|
||||||
|
case LimitSubjectSizeAssetsAll:
|
||||||
|
return u.Size.Assets.All()
|
||||||
|
case LimitSubjectSizeAssetsAttachmentsAll:
|
||||||
|
return u.Size.Assets.Attachments.All()
|
||||||
|
case LimitSubjectSizeAssetsAttachmentsIssues:
|
||||||
|
return u.Size.Assets.Attachments.Issues
|
||||||
|
case LimitSubjectSizeAssetsAttachmentsReleases:
|
||||||
|
return u.Size.Assets.Attachments.Releases
|
||||||
|
case LimitSubjectSizeAssetsArtifacts:
|
||||||
|
return u.Size.Assets.Artifacts
|
||||||
|
case LimitSubjectSizeAssetsPackagesAll:
|
||||||
|
return u.Size.Assets.Packages.All
|
||||||
|
case LimitSubjectSizeWiki:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeUserOwnedCondition(q string, userID int64) builder.Cond {
|
||||||
|
switch q {
|
||||||
|
case "repositories", "attachments", "artifacts":
|
||||||
|
return builder.Eq{"`repository`.owner_id": userID}
|
||||||
|
case "packages":
|
||||||
|
return builder.Or(
|
||||||
|
builder.Eq{"`repository`.owner_id": userID},
|
||||||
|
builder.And(
|
||||||
|
builder.Eq{"`package`.repo_id": 0},
|
||||||
|
builder.Eq{"`package`.owner_id": userID},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return builder.NewCond()
|
||||||
|
}
|
||||||
|
|
||||||
|
func createQueryFor(ctx context.Context, userID int64, q string) db.Engine {
|
||||||
|
session := db.GetEngine(ctx)
|
||||||
|
|
||||||
|
switch q {
|
||||||
|
case "repositories":
|
||||||
|
session = session.Table("repository")
|
||||||
|
case "attachments":
|
||||||
|
session = session.
|
||||||
|
Table("attachment").
|
||||||
|
Join("INNER", "`repository`", "`attachment`.repo_id = `repository`.id")
|
||||||
|
case "artifacts":
|
||||||
|
session = session.
|
||||||
|
Table("action_artifact").
|
||||||
|
Join("INNER", "`repository`", "`action_artifact`.repo_id = `repository`.id")
|
||||||
|
case "packages":
|
||||||
|
session = session.
|
||||||
|
Table("package_version").
|
||||||
|
Join("INNER", "`package_file`", "`package_file`.version_id = `package_version`.id").
|
||||||
|
Join("INNER", "`package_blob`", "`package_file`.blob_id = `package_blob`.id").
|
||||||
|
Join("INNER", "`package`", "`package_version`.package_id = `package`.id").
|
||||||
|
Join("LEFT OUTER", "`repository`", "`package`.repo_id = `repository`.id")
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.Where(makeUserOwnedCondition(q, userID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetQuotaAttachmentsForUser(ctx context.Context, userID int64, opts db.ListOptions) (int64, *[]*repo_model.Attachment, error) {
|
||||||
|
var attachments []*repo_model.Attachment
|
||||||
|
|
||||||
|
sess := createQueryFor(ctx, userID, "attachments").
|
||||||
|
OrderBy("`attachment`.size DESC")
|
||||||
|
if opts.PageSize > 0 {
|
||||||
|
sess = sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
|
||||||
|
}
|
||||||
|
count, err := sess.FindAndCount(&attachments)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, &attachments, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetQuotaPackagesForUser(ctx context.Context, userID int64, opts db.ListOptions) (int64, *[]*package_model.PackageVersion, error) {
|
||||||
|
var pkgs []*package_model.PackageVersion
|
||||||
|
|
||||||
|
sess := createQueryFor(ctx, userID, "packages").
|
||||||
|
OrderBy("`package_blob`.size DESC")
|
||||||
|
if opts.PageSize > 0 {
|
||||||
|
sess = sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
|
||||||
|
}
|
||||||
|
count, err := sess.FindAndCount(&pkgs)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, &pkgs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetQuotaArtifactsForUser(ctx context.Context, userID int64, opts db.ListOptions) (int64, *[]*action_model.ActionArtifact, error) {
|
||||||
|
var artifacts []*action_model.ActionArtifact
|
||||||
|
|
||||||
|
sess := createQueryFor(ctx, userID, "artifacts").
|
||||||
|
OrderBy("`action_artifact`.file_compressed_size DESC")
|
||||||
|
if opts.PageSize > 0 {
|
||||||
|
sess = sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
|
||||||
|
}
|
||||||
|
count, err := sess.FindAndCount(&artifacts)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, &artifacts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUsedForUser(ctx context.Context, userID int64) (*Used, error) {
|
||||||
|
var used Used
|
||||||
|
|
||||||
|
_, err := createQueryFor(ctx, userID, "repositories").
|
||||||
|
Where("`repository`.is_private = ?", true).
|
||||||
|
Select("SUM(git_size) AS code").
|
||||||
|
Get(&used.Size.Repos.Private)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = createQueryFor(ctx, userID, "repositories").
|
||||||
|
Where("`repository`.is_private = ?", false).
|
||||||
|
Select("SUM(git_size) AS code").
|
||||||
|
Get(&used.Size.Repos.Public)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = createQueryFor(ctx, userID, "repositories").
|
||||||
|
Select("SUM(lfs_size) AS lfs").
|
||||||
|
Get(&used.Size.Git.LFS)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = createQueryFor(ctx, userID, "attachments").
|
||||||
|
Select("SUM(`attachment`.size) AS size").
|
||||||
|
Where("`attachment`.release_id != 0").
|
||||||
|
Get(&used.Size.Assets.Attachments.Releases)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = createQueryFor(ctx, userID, "attachments").
|
||||||
|
Select("SUM(`attachment`.size) AS size").
|
||||||
|
Where("`attachment`.release_id = 0").
|
||||||
|
Get(&used.Size.Assets.Attachments.Issues)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = createQueryFor(ctx, userID, "artifacts").
|
||||||
|
Select("SUM(file_compressed_size) AS size").
|
||||||
|
Get(&used.Size.Assets.Artifacts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = createQueryFor(ctx, userID, "packages").
|
||||||
|
Select("SUM(package_blob.size) AS size").
|
||||||
|
Get(&used.Size.Assets.Packages.All)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &used, nil
|
||||||
|
}
|
17
modules/setting/quota.go
Normal file
17
modules/setting/quota.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
// Quota settings
|
||||||
|
var Quota = struct {
|
||||||
|
Enabled bool `ini:"ENABLED"`
|
||||||
|
DefaultGroups []string `ini:"DEFAULT_GROUPS"`
|
||||||
|
}{
|
||||||
|
Enabled: false,
|
||||||
|
DefaultGroups: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadQuotaFrom(rootCfg ConfigProvider) {
|
||||||
|
mustMapSetting(rootCfg, "quota", &Quota)
|
||||||
|
}
|
|
@ -155,6 +155,7 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
|
||||||
loadGitFrom(cfg)
|
loadGitFrom(cfg)
|
||||||
loadMirrorFrom(cfg)
|
loadMirrorFrom(cfg)
|
||||||
loadMarkupFrom(cfg)
|
loadMarkupFrom(cfg)
|
||||||
|
loadQuotaFrom(cfg)
|
||||||
loadOtherFrom(cfg)
|
loadOtherFrom(cfg)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
163
modules/structs/quota.go
Normal file
163
modules/structs/quota.go
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package structs
|
||||||
|
|
||||||
|
// QuotaInfo represents information about a user's quota
|
||||||
|
type QuotaInfo struct {
|
||||||
|
Used QuotaUsed `json:"used"`
|
||||||
|
Groups QuotaGroupList `json:"groups"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaUsed represents the quota usage of a user
|
||||||
|
type QuotaUsed struct {
|
||||||
|
Size QuotaUsedSize `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaUsedSize represents the size-based quota usage of a user
|
||||||
|
type QuotaUsedSize struct {
|
||||||
|
Repos QuotaUsedSizeRepos `json:"repos"`
|
||||||
|
Git QuotaUsedSizeGit `json:"git"`
|
||||||
|
Assets QuotaUsedSizeAssets `json:"assets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaUsedSizeRepos represents the size-based repository quota usage of a user
|
||||||
|
type QuotaUsedSizeRepos struct {
|
||||||
|
// Storage size of the user's public repositories
|
||||||
|
Public int64 `json:"public"`
|
||||||
|
// Storage size of the user's private repositories
|
||||||
|
Private int64 `json:"private"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaUsedSizeGit represents the size-based git (lfs) quota usage of a user
|
||||||
|
type QuotaUsedSizeGit struct {
|
||||||
|
// Storage size of the user's Git LFS objects
|
||||||
|
LFS int64 `json:"LFS"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaUsedSizeAssets represents the size-based asset usage of a user
|
||||||
|
type QuotaUsedSizeAssets struct {
|
||||||
|
Attachments QuotaUsedSizeAssetsAttachments `json:"attachments"`
|
||||||
|
// Storage size used for the user's artifacts
|
||||||
|
Artifacts int64 `json:"artifacts"`
|
||||||
|
Packages QuotaUsedSizeAssetsPackages `json:"packages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaUsedSizeAssetsAttachments represents the size-based attachment quota usage of a user
|
||||||
|
type QuotaUsedSizeAssetsAttachments struct {
|
||||||
|
// Storage size used for the user's issue & comment attachments
|
||||||
|
Issues int64 `json:"issues"`
|
||||||
|
// Storage size used for the user's release attachments
|
||||||
|
Releases int64 `json:"releases"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaUsedSizeAssetsPackages represents the size-based package quota usage of a user
|
||||||
|
type QuotaUsedSizeAssetsPackages struct {
|
||||||
|
// Storage suze used for the user's packages
|
||||||
|
All int64 `json:"all"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaRuleInfo contains information about a quota rule
|
||||||
|
type QuotaRuleInfo struct {
|
||||||
|
// Name of the rule (only shown to admins)
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
// The limit set by the rule
|
||||||
|
Limit int64 `json:"limit"`
|
||||||
|
// Subjects the rule affects
|
||||||
|
Subjects []string `json:"subjects,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaGroupList represents a list of quota groups
|
||||||
|
type QuotaGroupList []QuotaGroup
|
||||||
|
|
||||||
|
// QuotaGroup represents a quota group
|
||||||
|
type QuotaGroup struct {
|
||||||
|
// Name of the group
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
// Rules associated with the group
|
||||||
|
Rules []QuotaRuleInfo `json:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateQutaGroupOptions represents the options for creating a quota group
|
||||||
|
type CreateQuotaGroupOptions struct {
|
||||||
|
// Name of the quota group to create
|
||||||
|
Name string `json:"name" binding:"Required"`
|
||||||
|
// Rules to add to the newly created group.
|
||||||
|
// If a rule does not exist, it will be created.
|
||||||
|
Rules []CreateQuotaRuleOptions `json:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateQuotaRuleOptions represents the options for creating a quota rule
|
||||||
|
type CreateQuotaRuleOptions struct {
|
||||||
|
// Name of the rule to create
|
||||||
|
Name string `json:"name" binding:"Required"`
|
||||||
|
// The limit set by the rule
|
||||||
|
Limit *int64 `json:"limit"`
|
||||||
|
// The subjects affected by the rule
|
||||||
|
Subjects []string `json:"subjects"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditQuotaRuleOptions represents the options for editing a quota rule
|
||||||
|
type EditQuotaRuleOptions struct {
|
||||||
|
// The limit set by the rule
|
||||||
|
Limit *int64 `json:"limit"`
|
||||||
|
// The subjects affected by the rule
|
||||||
|
Subjects *[]string `json:"subjects"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserQuotaGroupsOptions represents the quota groups of a user
|
||||||
|
type SetUserQuotaGroupsOptions struct {
|
||||||
|
// Quota groups the user shall have
|
||||||
|
// required: true
|
||||||
|
Groups *[]string `json:"groups"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaUsedAttachmentList represents a list of attachment counting towards a user's quota
|
||||||
|
type QuotaUsedAttachmentList []*QuotaUsedAttachment
|
||||||
|
|
||||||
|
// QuotaUsedAttachment represents an attachment counting towards a user's quota
|
||||||
|
type QuotaUsedAttachment struct {
|
||||||
|
// Filename of the attachment
|
||||||
|
Name string `json:"name"`
|
||||||
|
// Size of the attachment (in bytes)
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
// API URL for the attachment
|
||||||
|
APIURL string `json:"api_url"`
|
||||||
|
// Context for the attachment: URLs to the containing object
|
||||||
|
ContainedIn struct {
|
||||||
|
// API URL for the object that contains this attachment
|
||||||
|
APIURL string `json:"api_url"`
|
||||||
|
// HTML URL for the object that contains this attachment
|
||||||
|
HTMLURL string `json:"html_url"`
|
||||||
|
} `json:"contained_in"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaUsedPackageList represents a list of packages counting towards a user's quota
|
||||||
|
type QuotaUsedPackageList []*QuotaUsedPackage
|
||||||
|
|
||||||
|
// QuotaUsedPackage represents a package counting towards a user's quota
|
||||||
|
type QuotaUsedPackage struct {
|
||||||
|
// Name of the package
|
||||||
|
Name string `json:"name"`
|
||||||
|
// Type of the package
|
||||||
|
Type string `json:"type"`
|
||||||
|
// Version of the package
|
||||||
|
Version string `json:"version"`
|
||||||
|
// Size of the package version
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
// HTML URL to the package version
|
||||||
|
HTMLURL string `json:"html_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaUsedArtifactList represents a list of artifacts counting towards a user's quota
|
||||||
|
type QuotaUsedArtifactList []*QuotaUsedArtifact
|
||||||
|
|
||||||
|
// QuotaUsedArtifact represents an artifact counting towards a user's quota
|
||||||
|
type QuotaUsedArtifact struct {
|
||||||
|
// Name of the artifact
|
||||||
|
Name string `json:"name"`
|
||||||
|
// Size of the artifact (compressed)
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
// HTML URL to the action run containing the artifact
|
||||||
|
HTMLURL string `json:"html_url"`
|
||||||
|
}
|
|
@ -115,6 +115,7 @@ loading = Loading…
|
||||||
|
|
||||||
error = Error
|
error = Error
|
||||||
error404 = The page you are trying to reach either <strong>does not exist</strong> or <strong>you are not authorized</strong> to view it.
|
error404 = The page you are trying to reach either <strong>does not exist</strong> or <strong>you are not authorized</strong> to view it.
|
||||||
|
error413 = You have exhausted your quota.
|
||||||
go_back = Go Back
|
go_back = Go Back
|
||||||
invalid_data = Invalid data: %v
|
invalid_data = Invalid data: %v
|
||||||
|
|
||||||
|
@ -2196,6 +2197,7 @@ settings.units.add_more = Add more...
|
||||||
|
|
||||||
settings.sync_mirror = Synchronize now
|
settings.sync_mirror = Synchronize now
|
||||||
settings.pull_mirror_sync_in_progress = Pulling changes from the remote %s at the moment.
|
settings.pull_mirror_sync_in_progress = Pulling changes from the remote %s at the moment.
|
||||||
|
settings.pull_mirror_sync_quota_exceeded = Quota exceeded, not pulling changes.
|
||||||
settings.push_mirror_sync_in_progress = Pushing changes to the remote %s at the moment.
|
settings.push_mirror_sync_in_progress = Pushing changes to the remote %s at the moment.
|
||||||
settings.site = Website
|
settings.site = Website
|
||||||
settings.update_settings = Save settings
|
settings.update_settings = Save settings
|
||||||
|
@ -2279,6 +2281,7 @@ settings.transfer_owner = New owner
|
||||||
settings.transfer_perform = Perform transfer
|
settings.transfer_perform = Perform transfer
|
||||||
settings.transfer_started = This repository has been marked for transfer and awaits confirmation from "%s"
|
settings.transfer_started = This repository has been marked for transfer and awaits confirmation from "%s"
|
||||||
settings.transfer_succeed = The repository has been transferred.
|
settings.transfer_succeed = The repository has been transferred.
|
||||||
|
settings.transfer_quota_exceeded = The new owner (%s) is over quota. The repository has not been transferred.
|
||||||
settings.signing_settings = Signing verification settings
|
settings.signing_settings = Signing verification settings
|
||||||
settings.trust_model = Signature trust model
|
settings.trust_model = Signature trust model
|
||||||
settings.trust_model.default = Default trust model
|
settings.trust_model.default = Default trust model
|
||||||
|
|
1
release-notes/4212.md
Normal file
1
release-notes/4212.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Added the foundations of a flexible, configurable quota system
|
|
@ -71,6 +71,7 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/actions"
|
"code.gitea.io/gitea/models/actions"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
@ -240,6 +241,18 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check the owner's quota
|
||||||
|
ok, err := quota_model.EvaluateForUser(ctx, ctx.ActionTask.OwnerID, quota_model.LimitSubjectSizeAssetsArtifacts)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("quota_model.EvaluateForUser: %v", err)
|
||||||
|
ctx.Error(http.StatusInternalServerError, "Error checking quota")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
ctx.Error(http.StatusRequestEntityTooLarge, "Quota exceeded")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// get upload file size
|
// get upload file size
|
||||||
fileRealTotalSize, contentLength := getUploadFileSize(ctx)
|
fileRealTotalSize, contentLength := getUploadFileSize(ctx)
|
||||||
|
|
||||||
|
|
|
@ -92,6 +92,7 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/actions"
|
"code.gitea.io/gitea/models/actions"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/storage"
|
"code.gitea.io/gitea/modules/storage"
|
||||||
|
@ -290,6 +291,18 @@ func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check the owner's quota
|
||||||
|
ok, err := quota_model.EvaluateForUser(ctx, task.OwnerID, quota_model.LimitSubjectSizeAssetsArtifacts)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("quota_model.EvaluateForUser: %v", err)
|
||||||
|
ctx.Error(http.StatusInternalServerError, "Error checking quota")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
ctx.Error(http.StatusRequestEntityTooLarge, "Quota exceeded")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
comp := ctx.Req.URL.Query().Get("comp")
|
comp := ctx.Req.URL.Query().Get("comp")
|
||||||
switch comp {
|
switch comp {
|
||||||
case "block", "appendBlock":
|
case "block", "appendBlock":
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
|
@ -74,6 +75,21 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func enforcePackagesQuota() func(ctx *context.Context) {
|
||||||
|
return func(ctx *context.Context) {
|
||||||
|
ok, err := quota_model.EvaluateForUser(ctx, ctx.Doer.ID, quota_model.LimitSubjectSizeAssetsPackagesAll)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("quota_model.EvaluateForUser: %v", err)
|
||||||
|
ctx.Error(http.StatusInternalServerError, "Error checking quota")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
ctx.Error(http.StatusRequestEntityTooLarge, "enforcePackagesQuota", "quota exceeded")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func verifyAuth(r *web.Route, authMethods []auth.Method) {
|
func verifyAuth(r *web.Route, authMethods []auth.Method) {
|
||||||
if setting.Service.EnableReverseProxyAuth {
|
if setting.Service.EnableReverseProxyAuth {
|
||||||
authMethods = append(authMethods, &auth.ReverseProxy{})
|
authMethods = append(authMethods, &auth.ReverseProxy{})
|
||||||
|
@ -111,7 +127,7 @@ func CommonRoutes() *web.Route {
|
||||||
r.Group("/alpine", func() {
|
r.Group("/alpine", func() {
|
||||||
r.Get("/key", alpine.GetRepositoryKey)
|
r.Get("/key", alpine.GetRepositoryKey)
|
||||||
r.Group("/{branch}/{repository}", func() {
|
r.Group("/{branch}/{repository}", func() {
|
||||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), alpine.UploadPackageFile)
|
r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), alpine.UploadPackageFile)
|
||||||
r.Group("/{architecture}", func() {
|
r.Group("/{architecture}", func() {
|
||||||
r.Get("/APKINDEX.tar.gz", alpine.GetRepositoryFile)
|
r.Get("/APKINDEX.tar.gz", alpine.GetRepositoryFile)
|
||||||
r.Group("/{filename}", func() {
|
r.Group("/{filename}", func() {
|
||||||
|
@ -124,12 +140,12 @@ func CommonRoutes() *web.Route {
|
||||||
r.Group("/cargo", func() {
|
r.Group("/cargo", func() {
|
||||||
r.Group("/api/v1/crates", func() {
|
r.Group("/api/v1/crates", func() {
|
||||||
r.Get("", cargo.SearchPackages)
|
r.Get("", cargo.SearchPackages)
|
||||||
r.Put("/new", reqPackageAccess(perm.AccessModeWrite), cargo.UploadPackage)
|
r.Put("/new", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), cargo.UploadPackage)
|
||||||
r.Group("/{package}", func() {
|
r.Group("/{package}", func() {
|
||||||
r.Group("/{version}", func() {
|
r.Group("/{version}", func() {
|
||||||
r.Get("/download", cargo.DownloadPackageFile)
|
r.Get("/download", cargo.DownloadPackageFile)
|
||||||
r.Delete("/yank", reqPackageAccess(perm.AccessModeWrite), cargo.YankPackage)
|
r.Delete("/yank", reqPackageAccess(perm.AccessModeWrite), cargo.YankPackage)
|
||||||
r.Put("/unyank", reqPackageAccess(perm.AccessModeWrite), cargo.UnyankPackage)
|
r.Put("/unyank", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), cargo.UnyankPackage)
|
||||||
})
|
})
|
||||||
r.Get("/owners", cargo.ListOwners)
|
r.Get("/owners", cargo.ListOwners)
|
||||||
})
|
})
|
||||||
|
@ -147,7 +163,7 @@ func CommonRoutes() *web.Route {
|
||||||
r.Get("/search", chef.EnumeratePackages)
|
r.Get("/search", chef.EnumeratePackages)
|
||||||
r.Group("/cookbooks", func() {
|
r.Group("/cookbooks", func() {
|
||||||
r.Get("", chef.EnumeratePackages)
|
r.Get("", chef.EnumeratePackages)
|
||||||
r.Post("", reqPackageAccess(perm.AccessModeWrite), chef.UploadPackage)
|
r.Post("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), chef.UploadPackage)
|
||||||
r.Group("/{name}", func() {
|
r.Group("/{name}", func() {
|
||||||
r.Get("", chef.PackageMetadata)
|
r.Get("", chef.PackageMetadata)
|
||||||
r.Group("/versions/{version}", func() {
|
r.Group("/versions/{version}", func() {
|
||||||
|
@ -167,7 +183,7 @@ func CommonRoutes() *web.Route {
|
||||||
r.Get("/p2/{vendorname}/{projectname}~dev.json", composer.PackageMetadata)
|
r.Get("/p2/{vendorname}/{projectname}~dev.json", composer.PackageMetadata)
|
||||||
r.Get("/p2/{vendorname}/{projectname}.json", composer.PackageMetadata)
|
r.Get("/p2/{vendorname}/{projectname}.json", composer.PackageMetadata)
|
||||||
r.Get("/files/{package}/{version}/{filename}", composer.DownloadPackageFile)
|
r.Get("/files/{package}/{version}/{filename}", composer.DownloadPackageFile)
|
||||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), composer.UploadPackage)
|
r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), composer.UploadPackage)
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
r.Group("/conan", func() {
|
r.Group("/conan", func() {
|
||||||
r.Group("/v1", func() {
|
r.Group("/v1", func() {
|
||||||
|
@ -183,14 +199,14 @@ func CommonRoutes() *web.Route {
|
||||||
r.Delete("", reqPackageAccess(perm.AccessModeWrite), conan.DeleteRecipeV1)
|
r.Delete("", reqPackageAccess(perm.AccessModeWrite), conan.DeleteRecipeV1)
|
||||||
r.Get("/search", conan.SearchPackagesV1)
|
r.Get("/search", conan.SearchPackagesV1)
|
||||||
r.Get("/digest", conan.RecipeDownloadURLs)
|
r.Get("/digest", conan.RecipeDownloadURLs)
|
||||||
r.Post("/upload_urls", reqPackageAccess(perm.AccessModeWrite), conan.RecipeUploadURLs)
|
r.Post("/upload_urls", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), conan.RecipeUploadURLs)
|
||||||
r.Get("/download_urls", conan.RecipeDownloadURLs)
|
r.Get("/download_urls", conan.RecipeDownloadURLs)
|
||||||
r.Group("/packages", func() {
|
r.Group("/packages", func() {
|
||||||
r.Post("/delete", reqPackageAccess(perm.AccessModeWrite), conan.DeletePackageV1)
|
r.Post("/delete", reqPackageAccess(perm.AccessModeWrite), conan.DeletePackageV1)
|
||||||
r.Group("/{package_reference}", func() {
|
r.Group("/{package_reference}", func() {
|
||||||
r.Get("", conan.PackageSnapshot)
|
r.Get("", conan.PackageSnapshot)
|
||||||
r.Get("/digest", conan.PackageDownloadURLs)
|
r.Get("/digest", conan.PackageDownloadURLs)
|
||||||
r.Post("/upload_urls", reqPackageAccess(perm.AccessModeWrite), conan.PackageUploadURLs)
|
r.Post("/upload_urls", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), conan.PackageUploadURLs)
|
||||||
r.Get("/download_urls", conan.PackageDownloadURLs)
|
r.Get("/download_urls", conan.PackageDownloadURLs)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -199,11 +215,11 @@ func CommonRoutes() *web.Route {
|
||||||
r.Group("/files/{name}/{version}/{user}/{channel}/{recipe_revision}", func() {
|
r.Group("/files/{name}/{version}/{user}/{channel}/{recipe_revision}", func() {
|
||||||
r.Group("/recipe/{filename}", func() {
|
r.Group("/recipe/{filename}", func() {
|
||||||
r.Get("", conan.DownloadRecipeFile)
|
r.Get("", conan.DownloadRecipeFile)
|
||||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), conan.UploadRecipeFile)
|
r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), conan.UploadRecipeFile)
|
||||||
})
|
})
|
||||||
r.Group("/package/{package_reference}/{package_revision}/{filename}", func() {
|
r.Group("/package/{package_reference}/{package_revision}/{filename}", func() {
|
||||||
r.Get("", conan.DownloadPackageFile)
|
r.Get("", conan.DownloadPackageFile)
|
||||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), conan.UploadPackageFile)
|
r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), conan.UploadPackageFile)
|
||||||
})
|
})
|
||||||
}, conan.ExtractPathParameters)
|
}, conan.ExtractPathParameters)
|
||||||
})
|
})
|
||||||
|
@ -228,7 +244,7 @@ func CommonRoutes() *web.Route {
|
||||||
r.Get("", conan.ListRecipeRevisionFiles)
|
r.Get("", conan.ListRecipeRevisionFiles)
|
||||||
r.Group("/{filename}", func() {
|
r.Group("/{filename}", func() {
|
||||||
r.Get("", conan.DownloadRecipeFile)
|
r.Get("", conan.DownloadRecipeFile)
|
||||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), conan.UploadRecipeFile)
|
r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), conan.UploadRecipeFile)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
r.Group("/packages", func() {
|
r.Group("/packages", func() {
|
||||||
|
@ -244,7 +260,7 @@ func CommonRoutes() *web.Route {
|
||||||
r.Get("", conan.ListPackageRevisionFiles)
|
r.Get("", conan.ListPackageRevisionFiles)
|
||||||
r.Group("/{filename}", func() {
|
r.Group("/{filename}", func() {
|
||||||
r.Get("", conan.DownloadPackageFile)
|
r.Get("", conan.DownloadPackageFile)
|
||||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), conan.UploadPackageFile)
|
r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), conan.UploadPackageFile)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -281,7 +297,7 @@ func CommonRoutes() *web.Route {
|
||||||
conda.DownloadPackageFile(ctx)
|
conda.DownloadPackageFile(ctx)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
r.Put("/*", reqPackageAccess(perm.AccessModeWrite), func(ctx *context.Context) {
|
r.Put("/*", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), func(ctx *context.Context) {
|
||||||
m := uploadPattern.FindStringSubmatch(ctx.Params("*"))
|
m := uploadPattern.FindStringSubmatch(ctx.Params("*"))
|
||||||
if len(m) == 0 {
|
if len(m) == 0 {
|
||||||
ctx.Status(http.StatusNotFound)
|
ctx.Status(http.StatusNotFound)
|
||||||
|
@ -301,7 +317,7 @@ func CommonRoutes() *web.Route {
|
||||||
r.Get("/PACKAGES{format}", cran.EnumerateSourcePackages)
|
r.Get("/PACKAGES{format}", cran.EnumerateSourcePackages)
|
||||||
r.Get("/{filename}", cran.DownloadSourcePackageFile)
|
r.Get("/{filename}", cran.DownloadSourcePackageFile)
|
||||||
})
|
})
|
||||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), cran.UploadSourcePackageFile)
|
r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), cran.UploadSourcePackageFile)
|
||||||
})
|
})
|
||||||
r.Group("/bin", func() {
|
r.Group("/bin", func() {
|
||||||
r.Group("/{platform}/contrib/{rversion}", func() {
|
r.Group("/{platform}/contrib/{rversion}", func() {
|
||||||
|
@ -309,7 +325,7 @@ func CommonRoutes() *web.Route {
|
||||||
r.Get("/PACKAGES{format}", cran.EnumerateBinaryPackages)
|
r.Get("/PACKAGES{format}", cran.EnumerateBinaryPackages)
|
||||||
r.Get("/{filename}", cran.DownloadBinaryPackageFile)
|
r.Get("/{filename}", cran.DownloadBinaryPackageFile)
|
||||||
})
|
})
|
||||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), cran.UploadBinaryPackageFile)
|
r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), cran.UploadBinaryPackageFile)
|
||||||
})
|
})
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
r.Group("/debian", func() {
|
r.Group("/debian", func() {
|
||||||
|
@ -325,13 +341,13 @@ func CommonRoutes() *web.Route {
|
||||||
r.Group("/pool/{distribution}/{component}", func() {
|
r.Group("/pool/{distribution}/{component}", func() {
|
||||||
r.Get("/{name}_{version}_{architecture}.deb", debian.DownloadPackageFile)
|
r.Get("/{name}_{version}_{architecture}.deb", debian.DownloadPackageFile)
|
||||||
r.Group("", func() {
|
r.Group("", func() {
|
||||||
r.Put("/upload", debian.UploadPackageFile)
|
r.Put("/upload", enforcePackagesQuota(), debian.UploadPackageFile)
|
||||||
r.Delete("/{name}/{version}/{architecture}", debian.DeletePackageFile)
|
r.Delete("/{name}/{version}/{architecture}", debian.DeletePackageFile)
|
||||||
}, reqPackageAccess(perm.AccessModeWrite))
|
}, reqPackageAccess(perm.AccessModeWrite))
|
||||||
})
|
})
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
r.Group("/go", func() {
|
r.Group("/go", func() {
|
||||||
r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), goproxy.UploadPackage)
|
r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), goproxy.UploadPackage)
|
||||||
r.Get("/sumdb/sum.golang.org/supported", func(ctx *context.Context) {
|
r.Get("/sumdb/sum.golang.org/supported", func(ctx *context.Context) {
|
||||||
ctx.Status(http.StatusNotFound)
|
ctx.Status(http.StatusNotFound)
|
||||||
})
|
})
|
||||||
|
@ -394,7 +410,7 @@ func CommonRoutes() *web.Route {
|
||||||
r.Group("/{filename}", func() {
|
r.Group("/{filename}", func() {
|
||||||
r.Get("", generic.DownloadPackageFile)
|
r.Get("", generic.DownloadPackageFile)
|
||||||
r.Group("", func() {
|
r.Group("", func() {
|
||||||
r.Put("", generic.UploadPackage)
|
r.Put("", enforcePackagesQuota(), generic.UploadPackage)
|
||||||
r.Delete("", generic.DeletePackageFile)
|
r.Delete("", generic.DeletePackageFile)
|
||||||
}, reqPackageAccess(perm.AccessModeWrite))
|
}, reqPackageAccess(perm.AccessModeWrite))
|
||||||
})
|
})
|
||||||
|
@ -403,10 +419,10 @@ func CommonRoutes() *web.Route {
|
||||||
r.Group("/helm", func() {
|
r.Group("/helm", func() {
|
||||||
r.Get("/index.yaml", helm.Index)
|
r.Get("/index.yaml", helm.Index)
|
||||||
r.Get("/{filename}", helm.DownloadPackageFile)
|
r.Get("/{filename}", helm.DownloadPackageFile)
|
||||||
r.Post("/api/charts", reqPackageAccess(perm.AccessModeWrite), helm.UploadPackage)
|
r.Post("/api/charts", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), helm.UploadPackage)
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
r.Group("/maven", func() {
|
r.Group("/maven", func() {
|
||||||
r.Put("/*", reqPackageAccess(perm.AccessModeWrite), maven.UploadPackageFile)
|
r.Put("/*", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), maven.UploadPackageFile)
|
||||||
r.Get("/*", maven.DownloadPackageFile)
|
r.Get("/*", maven.DownloadPackageFile)
|
||||||
r.Head("/*", maven.ProvidePackageFileHeader)
|
r.Head("/*", maven.ProvidePackageFileHeader)
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
|
@ -427,8 +443,8 @@ func CommonRoutes() *web.Route {
|
||||||
r.Get("/{version}/{filename}", nuget.DownloadPackageFile)
|
r.Get("/{version}/{filename}", nuget.DownloadPackageFile)
|
||||||
})
|
})
|
||||||
r.Group("", func() {
|
r.Group("", func() {
|
||||||
r.Put("/", nuget.UploadPackage)
|
r.Put("/", enforcePackagesQuota(), nuget.UploadPackage)
|
||||||
r.Put("/symbolpackage", nuget.UploadSymbolPackage)
|
r.Put("/symbolpackage", enforcePackagesQuota(), nuget.UploadSymbolPackage)
|
||||||
r.Delete("/{id}/{version}", nuget.DeletePackage)
|
r.Delete("/{id}/{version}", nuget.DeletePackage)
|
||||||
}, reqPackageAccess(perm.AccessModeWrite))
|
}, reqPackageAccess(perm.AccessModeWrite))
|
||||||
r.Get("/symbols/{filename}/{guid:[0-9a-fA-F]{32}[fF]{8}}/{filename2}", nuget.DownloadSymbolFile)
|
r.Get("/symbols/{filename}/{guid:[0-9a-fA-F]{32}[fF]{8}}/{filename2}", nuget.DownloadSymbolFile)
|
||||||
|
@ -450,7 +466,7 @@ func CommonRoutes() *web.Route {
|
||||||
r.Group("/npm", func() {
|
r.Group("/npm", func() {
|
||||||
r.Group("/@{scope}/{id}", func() {
|
r.Group("/@{scope}/{id}", func() {
|
||||||
r.Get("", npm.PackageMetadata)
|
r.Get("", npm.PackageMetadata)
|
||||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), npm.UploadPackage)
|
r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), npm.UploadPackage)
|
||||||
r.Group("/-/{version}/{filename}", func() {
|
r.Group("/-/{version}/{filename}", func() {
|
||||||
r.Get("", npm.DownloadPackageFile)
|
r.Get("", npm.DownloadPackageFile)
|
||||||
r.Delete("/-rev/{revision}", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
|
r.Delete("/-rev/{revision}", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
|
||||||
|
@ -463,7 +479,7 @@ func CommonRoutes() *web.Route {
|
||||||
})
|
})
|
||||||
r.Group("/{id}", func() {
|
r.Group("/{id}", func() {
|
||||||
r.Get("", npm.PackageMetadata)
|
r.Get("", npm.PackageMetadata)
|
||||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), npm.UploadPackage)
|
r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), npm.UploadPackage)
|
||||||
r.Group("/-/{version}/{filename}", func() {
|
r.Group("/-/{version}/{filename}", func() {
|
||||||
r.Get("", npm.DownloadPackageFile)
|
r.Get("", npm.DownloadPackageFile)
|
||||||
r.Delete("/-rev/{revision}", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
|
r.Delete("/-rev/{revision}", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
|
||||||
|
@ -496,7 +512,7 @@ func CommonRoutes() *web.Route {
|
||||||
r.Group("/api/packages", func() {
|
r.Group("/api/packages", func() {
|
||||||
r.Group("/versions/new", func() {
|
r.Group("/versions/new", func() {
|
||||||
r.Get("", pub.RequestUpload)
|
r.Get("", pub.RequestUpload)
|
||||||
r.Post("/upload", pub.UploadPackageFile)
|
r.Post("/upload", enforcePackagesQuota(), pub.UploadPackageFile)
|
||||||
r.Get("/finalize/{id}/{version}", pub.FinalizePackage)
|
r.Get("/finalize/{id}/{version}", pub.FinalizePackage)
|
||||||
}, reqPackageAccess(perm.AccessModeWrite))
|
}, reqPackageAccess(perm.AccessModeWrite))
|
||||||
r.Group("/{id}", func() {
|
r.Group("/{id}", func() {
|
||||||
|
@ -507,7 +523,7 @@ func CommonRoutes() *web.Route {
|
||||||
})
|
})
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
r.Group("/pypi", func() {
|
r.Group("/pypi", func() {
|
||||||
r.Post("/", reqPackageAccess(perm.AccessModeWrite), pypi.UploadPackageFile)
|
r.Post("/", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), pypi.UploadPackageFile)
|
||||||
r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile)
|
r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile)
|
||||||
r.Get("/simple/{id}", pypi.PackageMetadata)
|
r.Get("/simple/{id}", pypi.PackageMetadata)
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
|
@ -556,6 +572,10 @@ func CommonRoutes() *web.Route {
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
enforcePackagesQuota()(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.SetParams("group", strings.Trim(m[1], "/"))
|
ctx.SetParams("group", strings.Trim(m[1], "/"))
|
||||||
rpm.UploadPackageFile(ctx)
|
rpm.UploadPackageFile(ctx)
|
||||||
return
|
return
|
||||||
|
@ -591,7 +611,7 @@ func CommonRoutes() *web.Route {
|
||||||
r.Get("/quick/Marshal.4.8/{filename}", rubygems.ServePackageSpecification)
|
r.Get("/quick/Marshal.4.8/{filename}", rubygems.ServePackageSpecification)
|
||||||
r.Get("/gems/{filename}", rubygems.DownloadPackageFile)
|
r.Get("/gems/{filename}", rubygems.DownloadPackageFile)
|
||||||
r.Group("/api/v1/gems", func() {
|
r.Group("/api/v1/gems", func() {
|
||||||
r.Post("/", rubygems.UploadPackageFile)
|
r.Post("/", enforcePackagesQuota(), rubygems.UploadPackageFile)
|
||||||
r.Delete("/yank", rubygems.DeletePackage)
|
r.Delete("/yank", rubygems.DeletePackage)
|
||||||
}, reqPackageAccess(perm.AccessModeWrite))
|
}, reqPackageAccess(perm.AccessModeWrite))
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
|
@ -603,7 +623,7 @@ func CommonRoutes() *web.Route {
|
||||||
}, swift.CheckAcceptMediaType(swift.AcceptJSON))
|
}, swift.CheckAcceptMediaType(swift.AcceptJSON))
|
||||||
r.Group("/{version}", func() {
|
r.Group("/{version}", func() {
|
||||||
r.Get("/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest)
|
r.Get("/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest)
|
||||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile)
|
r.Put("", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), enforcePackagesQuota(), swift.UploadPackageFile)
|
||||||
r.Get("", func(ctx *context.Context) {
|
r.Get("", func(ctx *context.Context) {
|
||||||
// Can't use normal routes here: https://github.com/go-chi/chi/issues/781
|
// Can't use normal routes here: https://github.com/go-chi/chi/issues/781
|
||||||
|
|
||||||
|
@ -639,7 +659,7 @@ func CommonRoutes() *web.Route {
|
||||||
r.Get("", vagrant.EnumeratePackageVersions)
|
r.Get("", vagrant.EnumeratePackageVersions)
|
||||||
r.Group("/{version}/{provider}", func() {
|
r.Group("/{version}/{provider}", func() {
|
||||||
r.Get("", vagrant.DownloadPackageFile)
|
r.Get("", vagrant.DownloadPackageFile)
|
||||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), vagrant.UploadPackageFile)
|
r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), vagrant.UploadPackageFile)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
|
|
53
routers/api/v1/admin/quota.go
Normal file
53
routers/api/v1/admin/quota.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
"code.gitea.io/gitea/services/convert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetUserQuota return information about a user's quota
|
||||||
|
func GetUserQuota(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /admin/users/{username}/quota admin adminGetUserQuota
|
||||||
|
// ---
|
||||||
|
// summary: Get the user's quota info
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: username
|
||||||
|
// in: path
|
||||||
|
// description: username of user to query
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/QuotaInfo"
|
||||||
|
// "400":
|
||||||
|
// "$ref": "#/responses/error"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
used, err := quota_model.GetUsedForUser(ctx, ctx.ContextUser.ID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "quota_model.GetUsedForUser", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
groups, err := quota_model.GetGroupsForUser(ctx, ctx.ContextUser.ID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "quota_model.GetGroupsForUser", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := convert.ToQuotaInfo(used, groups, true)
|
||||||
|
ctx.JSON(http.StatusOK, &result)
|
||||||
|
}
|
436
routers/api/v1/admin/quota_group.go
Normal file
436
routers/api/v1/admin/quota_group.go
Normal file
|
@ -0,0 +1,436 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
go_context "context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/web"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
"code.gitea.io/gitea/services/convert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListQuotaGroups returns all the quota groups
|
||||||
|
func ListQuotaGroups(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /admin/quota/groups admin adminListQuotaGroups
|
||||||
|
// ---
|
||||||
|
// summary: List the available quota groups
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/QuotaGroupList"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
|
||||||
|
groups, err := quota_model.ListGroups(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "quota_model.ListGroups", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, group := range groups {
|
||||||
|
if err = group.LoadRules(ctx); err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "quota_model.group.LoadRules", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, convert.ToQuotaGroupList(groups, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
func createQuotaGroupWithRules(ctx go_context.Context, opts *api.CreateQuotaGroupOptions) (*quota_model.Group, error) {
|
||||||
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer committer.Close()
|
||||||
|
|
||||||
|
group, err := quota_model.CreateGroup(ctx, opts.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rule := range opts.Rules {
|
||||||
|
exists, err := quota_model.DoesRuleExist(ctx, rule.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
var limit int64
|
||||||
|
if rule.Limit != nil {
|
||||||
|
limit = *rule.Limit
|
||||||
|
}
|
||||||
|
|
||||||
|
subjects, err := toLimitSubjects(rule.Subjects)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = quota_model.CreateRule(ctx, rule.Name, limit, *subjects)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = group.AddRuleByName(ctx, rule.Name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = group.LoadRules(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return group, committer.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateQuotaGroup creates a new quota group
|
||||||
|
func CreateQuotaGroup(ctx *context.APIContext) {
|
||||||
|
// swagger:operation POST /admin/quota/groups admin adminCreateQuotaGroup
|
||||||
|
// ---
|
||||||
|
// summary: Create a new quota group
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: group
|
||||||
|
// in: body
|
||||||
|
// description: Definition of the quota group
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/CreateQuotaGroupOptions"
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "201":
|
||||||
|
// "$ref": "#/responses/QuotaGroup"
|
||||||
|
// "400":
|
||||||
|
// "$ref": "#/responses/error"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "409":
|
||||||
|
// "$ref": "#/responses/error"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
form := web.GetForm(ctx).(*api.CreateQuotaGroupOptions)
|
||||||
|
|
||||||
|
group, err := createQuotaGroupWithRules(ctx, form)
|
||||||
|
if err != nil {
|
||||||
|
if quota_model.IsErrGroupAlreadyExists(err) {
|
||||||
|
ctx.Error(http.StatusConflict, "", err)
|
||||||
|
} else if quota_model.IsErrParseLimitSubjectUnrecognized(err) {
|
||||||
|
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||||
|
} else {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "quota_model.CreateGroup", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSON(http.StatusCreated, convert.ToQuotaGroup(*group, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUsersInQuotaGroup lists all the users in a quota group
|
||||||
|
func ListUsersInQuotaGroup(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /admin/quota/groups/{quotagroup}/users admin adminListUsersInQuotaGroup
|
||||||
|
// ---
|
||||||
|
// summary: List users in a quota group
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: quotagroup
|
||||||
|
// in: path
|
||||||
|
// description: quota group to list members of
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/UserList"
|
||||||
|
// "400":
|
||||||
|
// "$ref": "#/responses/error"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
users, err := quota_model.ListUsersInGroup(ctx, ctx.QuotaGroup.Name)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "quota_model.ListUsersInGroup", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSON(http.StatusOK, convert.ToUsers(ctx, ctx.Doer, users))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddUserToQuotaGroup adds a user to a quota group
|
||||||
|
func AddUserToQuotaGroup(ctx *context.APIContext) {
|
||||||
|
// swagger:operation PUT /admin/quota/groups/{quotagroup}/users/{username} admin adminAddUserToQuotaGroup
|
||||||
|
// ---
|
||||||
|
// summary: Add a user to a quota group
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: quotagroup
|
||||||
|
// in: path
|
||||||
|
// description: quota group to add the user to
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: username
|
||||||
|
// in: path
|
||||||
|
// description: username of the user to add to the quota group
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "204":
|
||||||
|
// "$ref": "#/responses/empty"
|
||||||
|
// "400":
|
||||||
|
// "$ref": "#/responses/error"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "409":
|
||||||
|
// "$ref": "#/responses/error"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
err := ctx.QuotaGroup.AddUserByID(ctx, ctx.ContextUser.ID)
|
||||||
|
if err != nil {
|
||||||
|
if quota_model.IsErrUserAlreadyInGroup(err) {
|
||||||
|
ctx.Error(http.StatusConflict, "", err)
|
||||||
|
} else {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "quota_group.group.AddUserByID", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveUserFromQuotaGroup removes a user from a quota group
|
||||||
|
func RemoveUserFromQuotaGroup(ctx *context.APIContext) {
|
||||||
|
// swagger:operation DELETE /admin/quota/groups/{quotagroup}/users/{username} admin adminRemoveUserFromQuotaGroup
|
||||||
|
// ---
|
||||||
|
// summary: Remove a user from a quota group
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: quotagroup
|
||||||
|
// in: path
|
||||||
|
// description: quota group to remove a user from
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: username
|
||||||
|
// in: path
|
||||||
|
// description: username of the user to add to the quota group
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "204":
|
||||||
|
// "$ref": "#/responses/empty"
|
||||||
|
// "400":
|
||||||
|
// "$ref": "#/responses/error"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
err := ctx.QuotaGroup.RemoveUserByID(ctx, ctx.ContextUser.ID)
|
||||||
|
if err != nil {
|
||||||
|
if quota_model.IsErrUserNotInGroup(err) {
|
||||||
|
ctx.NotFound()
|
||||||
|
} else {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "quota_model.group.RemoveUserByID", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserQuotaGroups moves the user to specific quota groups
|
||||||
|
func SetUserQuotaGroups(ctx *context.APIContext) {
|
||||||
|
// swagger:operation POST /admin/users/{username}/quota/groups admin adminSetUserQuotaGroups
|
||||||
|
// ---
|
||||||
|
// summary: Set the user's quota groups to a given list.
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: username
|
||||||
|
// in: path
|
||||||
|
// description: username of the user to add to the quota group
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: groups
|
||||||
|
// in: body
|
||||||
|
// description: quota group to remove a user from
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/SetUserQuotaGroupsOptions"
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "204":
|
||||||
|
// "$ref": "#/responses/empty"
|
||||||
|
// "400":
|
||||||
|
// "$ref": "#/responses/error"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
form := web.GetForm(ctx).(*api.SetUserQuotaGroupsOptions)
|
||||||
|
|
||||||
|
err := quota_model.SetUserGroups(ctx, ctx.ContextUser.ID, form.Groups)
|
||||||
|
if err != nil {
|
||||||
|
if quota_model.IsErrGroupNotFound(err) {
|
||||||
|
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||||
|
} else {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "quota_model.SetUserGroups", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteQuotaGroup deletes a quota group
|
||||||
|
func DeleteQuotaGroup(ctx *context.APIContext) {
|
||||||
|
// swagger:operation DELETE /admin/quota/groups/{quotagroup} admin adminDeleteQuotaGroup
|
||||||
|
// ---
|
||||||
|
// summary: Delete a quota group
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: quotagroup
|
||||||
|
// in: path
|
||||||
|
// description: quota group to delete
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "204":
|
||||||
|
// "$ref": "#/responses/empty"
|
||||||
|
// "400":
|
||||||
|
// "$ref": "#/responses/error"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
err := quota_model.DeleteGroupByName(ctx, ctx.QuotaGroup.Name)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "quota_model.DeleteGroupByName", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQuotaGroup returns information about a quota group
|
||||||
|
func GetQuotaGroup(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /admin/quota/groups/{quotagroup} admin adminGetQuotaGroup
|
||||||
|
// ---
|
||||||
|
// summary: Get information about the quota group
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: quotagroup
|
||||||
|
// in: path
|
||||||
|
// description: quota group to query
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/QuotaGroup"
|
||||||
|
// "400":
|
||||||
|
// "$ref": "#/responses/error"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, convert.ToQuotaGroup(*ctx.QuotaGroup, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRuleToQuotaGroup adds a rule to a quota group
|
||||||
|
func AddRuleToQuotaGroup(ctx *context.APIContext) {
|
||||||
|
// swagger:operation PUT /admin/quota/groups/{quotagroup}/rules/{quotarule} admin adminAddRuleToQuotaGroup
|
||||||
|
// ---
|
||||||
|
// summary: Adds a rule to a quota group
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: quotagroup
|
||||||
|
// in: path
|
||||||
|
// description: quota group to add a rule to
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: quotarule
|
||||||
|
// in: path
|
||||||
|
// description: the name of the quota rule to add to the group
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "204":
|
||||||
|
// "$ref": "#/responses/empty"
|
||||||
|
// "400":
|
||||||
|
// "$ref": "#/responses/error"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "409":
|
||||||
|
// "$ref": "#/responses/error"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
err := ctx.QuotaGroup.AddRuleByName(ctx, ctx.QuotaRule.Name)
|
||||||
|
if err != nil {
|
||||||
|
if quota_model.IsErrRuleAlreadyInGroup(err) {
|
||||||
|
ctx.Error(http.StatusConflict, "", err)
|
||||||
|
} else if quota_model.IsErrRuleNotFound(err) {
|
||||||
|
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||||
|
} else {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "quota_model.group.AddRuleByName", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveRuleFromQuotaGroup removes a rule from a quota group
|
||||||
|
func RemoveRuleFromQuotaGroup(ctx *context.APIContext) {
|
||||||
|
// swagger:operation DELETE /admin/quota/groups/{quotagroup}/rules/{quotarule} admin adminRemoveRuleFromQuotaGroup
|
||||||
|
// ---
|
||||||
|
// summary: Removes a rule from a quota group
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: quotagroup
|
||||||
|
// in: path
|
||||||
|
// description: quota group to add a rule to
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: quotarule
|
||||||
|
// in: path
|
||||||
|
// description: the name of the quota rule to remove from the group
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "201":
|
||||||
|
// "$ref": "#/responses/empty"
|
||||||
|
// "400":
|
||||||
|
// "$ref": "#/responses/error"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
err := ctx.QuotaGroup.RemoveRuleByName(ctx, ctx.QuotaRule.Name)
|
||||||
|
if err != nil {
|
||||||
|
if quota_model.IsErrRuleNotInGroup(err) {
|
||||||
|
ctx.NotFound()
|
||||||
|
} else {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "quota_model.group.RemoveRuleByName", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
}
|
219
routers/api/v1/admin/quota_rule.go
Normal file
219
routers/api/v1/admin/quota_rule.go
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/web"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
"code.gitea.io/gitea/services/convert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func toLimitSubjects(subjStrings []string) (*quota_model.LimitSubjects, error) {
|
||||||
|
subjects := make(quota_model.LimitSubjects, len(subjStrings))
|
||||||
|
for i := range len(subjStrings) {
|
||||||
|
subj, err := quota_model.ParseLimitSubject(subjStrings[i])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
subjects[i] = subj
|
||||||
|
}
|
||||||
|
|
||||||
|
return &subjects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListQuotaRules lists all the quota rules
|
||||||
|
func ListQuotaRules(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /admin/quota/rules admin adminListQuotaRules
|
||||||
|
// ---
|
||||||
|
// summary: List the available quota rules
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/QuotaRuleInfoList"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
|
||||||
|
rules, err := quota_model.ListRules(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "quota_model.ListQuotaRules", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]api.QuotaRuleInfo, len(rules))
|
||||||
|
for i := range len(rules) {
|
||||||
|
result[i] = convert.ToQuotaRuleInfo(rules[i], true)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateQuotaRule creates a new quota rule
|
||||||
|
func CreateQuotaRule(ctx *context.APIContext) {
|
||||||
|
// swagger:operation POST /admin/quota/rules admin adminCreateQuotaRule
|
||||||
|
// ---
|
||||||
|
// summary: Create a new quota rule
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: rule
|
||||||
|
// in: body
|
||||||
|
// description: Definition of the quota rule
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/CreateQuotaRuleOptions"
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "201":
|
||||||
|
// "$ref": "#/responses/QuotaRuleInfo"
|
||||||
|
// "400":
|
||||||
|
// "$ref": "#/responses/error"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "409":
|
||||||
|
// "$ref": "#/responses/error"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
form := web.GetForm(ctx).(*api.CreateQuotaRuleOptions)
|
||||||
|
|
||||||
|
if form.Limit == nil {
|
||||||
|
ctx.Error(http.StatusUnprocessableEntity, "quota_model.ParseLimitSubject", fmt.Errorf("[Limit]: Required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subjects, err := toLimitSubjects(form.Subjects)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusUnprocessableEntity, "quota_model.ParseLimitSubject", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rule, err := quota_model.CreateRule(ctx, form.Name, *form.Limit, *subjects)
|
||||||
|
if err != nil {
|
||||||
|
if quota_model.IsErrRuleAlreadyExists(err) {
|
||||||
|
ctx.Error(http.StatusConflict, "", err)
|
||||||
|
} else {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "quota_model.CreateRule", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSON(http.StatusCreated, convert.ToQuotaRuleInfo(*rule, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQuotaRule returns information about the specified quota rule
|
||||||
|
func GetQuotaRule(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /admin/quota/rules/{quotarule} admin adminGetQuotaRule
|
||||||
|
// ---
|
||||||
|
// summary: Get information about a quota rule
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: quotarule
|
||||||
|
// in: path
|
||||||
|
// description: quota rule to query
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/QuotaRuleInfo"
|
||||||
|
// "400":
|
||||||
|
// "$ref": "#/responses/error"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, convert.ToQuotaRuleInfo(*ctx.QuotaRule, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditQuotaRule changes an existing quota rule
|
||||||
|
func EditQuotaRule(ctx *context.APIContext) {
|
||||||
|
// swagger:operation PATCH /admin/quota/rules/{quotarule} admin adminEditQuotaRule
|
||||||
|
// ---
|
||||||
|
// summary: Change an existing quota rule
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: quotarule
|
||||||
|
// in: path
|
||||||
|
// description: Quota rule to change
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: rule
|
||||||
|
// in: body
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/EditQuotaRuleOptions"
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/QuotaRuleInfo"
|
||||||
|
// "400":
|
||||||
|
// "$ref": "#/responses/error"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
form := web.GetForm(ctx).(*api.EditQuotaRuleOptions)
|
||||||
|
|
||||||
|
var subjects *quota_model.LimitSubjects
|
||||||
|
if form.Subjects != nil {
|
||||||
|
subjs := make(quota_model.LimitSubjects, len(*form.Subjects))
|
||||||
|
for i := range len(*form.Subjects) {
|
||||||
|
subj, err := quota_model.ParseLimitSubject((*form.Subjects)[i])
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusUnprocessableEntity, "quota_model.ParseLimitSubject", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
subjs[i] = subj
|
||||||
|
}
|
||||||
|
subjects = &subjs
|
||||||
|
}
|
||||||
|
|
||||||
|
rule, err := ctx.QuotaRule.Edit(ctx, form.Limit, subjects)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "quota_model.rule.Edit", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, convert.ToQuotaRuleInfo(*rule, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteQuotaRule deletes a quota rule
|
||||||
|
func DeleteQuotaRule(ctx *context.APIContext) {
|
||||||
|
// swagger:operation DELETE /admin/quota/rules/{quotarule} admin adminDEleteQuotaRule
|
||||||
|
// ---
|
||||||
|
// summary: Deletes a quota rule
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: quotarule
|
||||||
|
// in: path
|
||||||
|
// description: quota rule to delete
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "204":
|
||||||
|
// "$ref": "#/responses/empty"
|
||||||
|
// "400":
|
||||||
|
// "$ref": "#/responses/error"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
err := quota_model.DeleteRuleByName(ctx, ctx.QuotaRule.Name)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "quota_model.DeleteRuleByName", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||||
// Copyright 2016 The Gitea Authors. All rights reserved.
|
// Copyright 2016 The Gitea Authors. All rights reserved.
|
||||||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
// Copyright 2023-2024 The Forgejo Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
// Package v1 Gitea API
|
// Package v1 Gitea API
|
||||||
|
@ -77,6 +77,7 @@ import (
|
||||||
"code.gitea.io/gitea/models/organization"
|
"code.gitea.io/gitea/models/organization"
|
||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
@ -892,6 +893,15 @@ func Routes() *web.Route {
|
||||||
// Users (requires user scope)
|
// Users (requires user scope)
|
||||||
m.Group("/user", func() {
|
m.Group("/user", func() {
|
||||||
m.Get("", user.GetAuthenticatedUser)
|
m.Get("", user.GetAuthenticatedUser)
|
||||||
|
if setting.Quota.Enabled {
|
||||||
|
m.Group("/quota", func() {
|
||||||
|
m.Get("", user.GetQuota)
|
||||||
|
m.Get("/check", user.CheckQuota)
|
||||||
|
m.Get("/attachments", user.ListQuotaAttachments)
|
||||||
|
m.Get("/packages", user.ListQuotaPackages)
|
||||||
|
m.Get("/artifacts", user.ListQuotaArtifacts)
|
||||||
|
})
|
||||||
|
}
|
||||||
m.Group("/settings", func() {
|
m.Group("/settings", func() {
|
||||||
m.Get("", user.GetUserSettings)
|
m.Get("", user.GetUserSettings)
|
||||||
m.Patch("", bind(api.UserSettingsOptions{}), user.UpdateUserSettings)
|
m.Patch("", bind(api.UserSettingsOptions{}), user.UpdateUserSettings)
|
||||||
|
@ -964,7 +974,7 @@ func Routes() *web.Route {
|
||||||
|
|
||||||
// (repo scope)
|
// (repo scope)
|
||||||
m.Combo("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(user.ListMyRepos).
|
m.Combo("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(user.ListMyRepos).
|
||||||
Post(bind(api.CreateRepoOption{}), repo.Create)
|
Post(bind(api.CreateRepoOption{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetUser), repo.Create)
|
||||||
|
|
||||||
// (repo scope)
|
// (repo scope)
|
||||||
if !setting.Repository.DisableStars {
|
if !setting.Repository.DisableStars {
|
||||||
|
@ -1095,7 +1105,7 @@ func Routes() *web.Route {
|
||||||
m.Get("", repo.ListBranches)
|
m.Get("", repo.ListBranches)
|
||||||
m.Get("/*", repo.GetBranch)
|
m.Get("/*", repo.GetBranch)
|
||||||
m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch)
|
m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch)
|
||||||
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), repo.CreateBranch)
|
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeGitAll, context.QuotaTargetRepo), repo.CreateBranch)
|
||||||
}, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode))
|
}, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode))
|
||||||
m.Group("/branch_protections", func() {
|
m.Group("/branch_protections", func() {
|
||||||
m.Get("", repo.ListBranchProtections)
|
m.Get("", repo.ListBranchProtections)
|
||||||
|
@ -1109,7 +1119,7 @@ func Routes() *web.Route {
|
||||||
m.Group("/tags", func() {
|
m.Group("/tags", func() {
|
||||||
m.Get("", repo.ListTags)
|
m.Get("", repo.ListTags)
|
||||||
m.Get("/*", repo.GetTag)
|
m.Get("/*", repo.GetTag)
|
||||||
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateTagOption{}), repo.CreateTag)
|
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateTagOption{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.CreateTag)
|
||||||
m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteTag)
|
m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteTag)
|
||||||
}, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true))
|
}, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true))
|
||||||
m.Group("/tag_protections", func() {
|
m.Group("/tag_protections", func() {
|
||||||
|
@ -1143,10 +1153,10 @@ func Routes() *web.Route {
|
||||||
m.Group("/wiki", func() {
|
m.Group("/wiki", func() {
|
||||||
m.Combo("/page/{pageName}").
|
m.Combo("/page/{pageName}").
|
||||||
Get(repo.GetWikiPage).
|
Get(repo.GetWikiPage).
|
||||||
Patch(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.EditWikiPage).
|
Patch(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeWiki, context.QuotaTargetRepo), repo.EditWikiPage).
|
||||||
Delete(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), repo.DeleteWikiPage)
|
Delete(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), repo.DeleteWikiPage)
|
||||||
m.Get("/revisions/{pageName}", repo.ListPageRevisions)
|
m.Get("/revisions/{pageName}", repo.ListPageRevisions)
|
||||||
m.Post("/new", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.NewWikiPage)
|
m.Post("/new", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeWiki, context.QuotaTargetRepo), repo.NewWikiPage)
|
||||||
m.Get("/pages", repo.ListWikiPages)
|
m.Get("/pages", repo.ListWikiPages)
|
||||||
}, mustEnableWiki)
|
}, mustEnableWiki)
|
||||||
m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup)
|
m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup)
|
||||||
|
@ -1163,15 +1173,15 @@ func Routes() *web.Route {
|
||||||
}, reqToken())
|
}, reqToken())
|
||||||
m.Group("/releases", func() {
|
m.Group("/releases", func() {
|
||||||
m.Combo("").Get(repo.ListReleases).
|
m.Combo("").Get(repo.ListReleases).
|
||||||
Post(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.CreateReleaseOption{}), repo.CreateRelease)
|
Post(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.CreateReleaseOption{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.CreateRelease)
|
||||||
m.Combo("/latest").Get(repo.GetLatestRelease)
|
m.Combo("/latest").Get(repo.GetLatestRelease)
|
||||||
m.Group("/{id}", func() {
|
m.Group("/{id}", func() {
|
||||||
m.Combo("").Get(repo.GetRelease).
|
m.Combo("").Get(repo.GetRelease).
|
||||||
Patch(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.EditReleaseOption{}), repo.EditRelease).
|
Patch(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.EditReleaseOption{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.EditRelease).
|
||||||
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteRelease)
|
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteRelease)
|
||||||
m.Group("/assets", func() {
|
m.Group("/assets", func() {
|
||||||
m.Combo("").Get(repo.ListReleaseAttachments).
|
m.Combo("").Get(repo.ListReleaseAttachments).
|
||||||
Post(reqToken(), reqRepoWriter(unit.TypeReleases), repo.CreateReleaseAttachment)
|
Post(reqToken(), reqRepoWriter(unit.TypeReleases), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeAssetsAttachmentsReleases, context.QuotaTargetRepo), repo.CreateReleaseAttachment)
|
||||||
m.Combo("/{attachment_id}").Get(repo.GetReleaseAttachment).
|
m.Combo("/{attachment_id}").Get(repo.GetReleaseAttachment).
|
||||||
Patch(reqToken(), reqRepoWriter(unit.TypeReleases), bind(api.EditAttachmentOptions{}), repo.EditReleaseAttachment).
|
Patch(reqToken(), reqRepoWriter(unit.TypeReleases), bind(api.EditAttachmentOptions{}), repo.EditReleaseAttachment).
|
||||||
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseAttachment)
|
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseAttachment)
|
||||||
|
@ -1183,7 +1193,7 @@ func Routes() *web.Route {
|
||||||
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseByTag)
|
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseByTag)
|
||||||
})
|
})
|
||||||
}, reqRepoReader(unit.TypeReleases))
|
}, reqRepoReader(unit.TypeReleases))
|
||||||
m.Post("/mirror-sync", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.MirrorSync)
|
m.Post("/mirror-sync", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeGitAll, context.QuotaTargetRepo), repo.MirrorSync)
|
||||||
m.Post("/push_mirrors-sync", reqAdmin(), reqToken(), mustNotBeArchived, repo.PushMirrorSync)
|
m.Post("/push_mirrors-sync", reqAdmin(), reqToken(), mustNotBeArchived, repo.PushMirrorSync)
|
||||||
m.Group("/push_mirrors", func() {
|
m.Group("/push_mirrors", func() {
|
||||||
m.Combo("").Get(repo.ListPushMirrors).
|
m.Combo("").Get(repo.ListPushMirrors).
|
||||||
|
@ -1202,11 +1212,11 @@ func Routes() *web.Route {
|
||||||
m.Combo("").Get(repo.GetPullRequest).
|
m.Combo("").Get(repo.GetPullRequest).
|
||||||
Patch(reqToken(), bind(api.EditPullRequestOption{}), repo.EditPullRequest)
|
Patch(reqToken(), bind(api.EditPullRequestOption{}), repo.EditPullRequest)
|
||||||
m.Get(".{diffType:diff|patch}", repo.DownloadPullDiffOrPatch)
|
m.Get(".{diffType:diff|patch}", repo.DownloadPullDiffOrPatch)
|
||||||
m.Post("/update", reqToken(), repo.UpdatePullRequest)
|
m.Post("/update", reqToken(), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeGitAll, context.QuotaTargetRepo), repo.UpdatePullRequest)
|
||||||
m.Get("/commits", repo.GetPullRequestCommits)
|
m.Get("/commits", repo.GetPullRequestCommits)
|
||||||
m.Get("/files", repo.GetPullRequestFiles)
|
m.Get("/files", repo.GetPullRequestFiles)
|
||||||
m.Combo("/merge").Get(repo.IsPullRequestMerged).
|
m.Combo("/merge").Get(repo.IsPullRequestMerged).
|
||||||
Post(reqToken(), mustNotBeArchived, bind(forms.MergePullRequestForm{}), repo.MergePullRequest).
|
Post(reqToken(), mustNotBeArchived, bind(forms.MergePullRequestForm{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeGitAll, context.QuotaTargetRepo), repo.MergePullRequest).
|
||||||
Delete(reqToken(), mustNotBeArchived, repo.CancelScheduledAutoMerge)
|
Delete(reqToken(), mustNotBeArchived, repo.CancelScheduledAutoMerge)
|
||||||
m.Group("/reviews", func() {
|
m.Group("/reviews", func() {
|
||||||
m.Combo("").
|
m.Combo("").
|
||||||
|
@ -1261,15 +1271,15 @@ func Routes() *web.Route {
|
||||||
m.Get("/tags/{sha}", repo.GetAnnotatedTag)
|
m.Get("/tags/{sha}", repo.GetAnnotatedTag)
|
||||||
m.Get("/notes/{sha}", repo.GetNote)
|
m.Get("/notes/{sha}", repo.GetNote)
|
||||||
}, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode))
|
}, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode))
|
||||||
m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), mustNotBeArchived, repo.ApplyDiffPatch)
|
m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.ApplyDiffPatch)
|
||||||
m.Group("/contents", func() {
|
m.Group("/contents", func() {
|
||||||
m.Get("", repo.GetContentsList)
|
m.Get("", repo.GetContentsList)
|
||||||
m.Post("", reqToken(), bind(api.ChangeFilesOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.ChangeFiles)
|
m.Post("", reqToken(), bind(api.ChangeFilesOptions{}), reqRepoBranchWriter, mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.ChangeFiles)
|
||||||
m.Get("/*", repo.GetContents)
|
m.Get("/*", repo.GetContents)
|
||||||
m.Group("/*", func() {
|
m.Group("/*", func() {
|
||||||
m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.CreateFile)
|
m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.CreateFile)
|
||||||
m.Put("", bind(api.UpdateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.UpdateFile)
|
m.Put("", bind(api.UpdateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.UpdateFile)
|
||||||
m.Delete("", bind(api.DeleteFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.DeleteFile)
|
m.Delete("", bind(api.DeleteFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.DeleteFile)
|
||||||
}, reqToken())
|
}, reqToken())
|
||||||
}, reqRepoReader(unit.TypeCode))
|
}, reqRepoReader(unit.TypeCode))
|
||||||
m.Get("/signing-key.gpg", misc.SigningKey)
|
m.Get("/signing-key.gpg", misc.SigningKey)
|
||||||
|
@ -1326,7 +1336,7 @@ func Routes() *web.Route {
|
||||||
m.Group("/assets", func() {
|
m.Group("/assets", func() {
|
||||||
m.Combo("").
|
m.Combo("").
|
||||||
Get(repo.ListIssueCommentAttachments).
|
Get(repo.ListIssueCommentAttachments).
|
||||||
Post(reqToken(), mustNotBeArchived, repo.CreateIssueCommentAttachment)
|
Post(reqToken(), mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeAssetsAttachmentsIssues, context.QuotaTargetRepo), repo.CreateIssueCommentAttachment)
|
||||||
m.Combo("/{attachment_id}").
|
m.Combo("/{attachment_id}").
|
||||||
Get(repo.GetIssueCommentAttachment).
|
Get(repo.GetIssueCommentAttachment).
|
||||||
Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueCommentAttachment).
|
Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueCommentAttachment).
|
||||||
|
@ -1378,7 +1388,7 @@ func Routes() *web.Route {
|
||||||
m.Group("/assets", func() {
|
m.Group("/assets", func() {
|
||||||
m.Combo("").
|
m.Combo("").
|
||||||
Get(repo.ListIssueAttachments).
|
Get(repo.ListIssueAttachments).
|
||||||
Post(reqToken(), mustNotBeArchived, repo.CreateIssueAttachment)
|
Post(reqToken(), mustNotBeArchived, context.EnforceQuotaAPI(quota_model.LimitSubjectSizeAssetsAttachmentsIssues, context.QuotaTargetRepo), repo.CreateIssueAttachment)
|
||||||
m.Combo("/{attachment_id}").
|
m.Combo("/{attachment_id}").
|
||||||
Get(repo.GetIssueAttachment).
|
Get(repo.GetIssueAttachment).
|
||||||
Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueAttachment).
|
Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueAttachment).
|
||||||
|
@ -1440,7 +1450,7 @@ func Routes() *web.Route {
|
||||||
Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit).
|
Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit).
|
||||||
Delete(reqToken(), reqOrgOwnership(), org.Delete)
|
Delete(reqToken(), reqOrgOwnership(), org.Delete)
|
||||||
m.Combo("/repos").Get(user.ListOrgRepos).
|
m.Combo("/repos").Get(user.ListOrgRepos).
|
||||||
Post(reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo)
|
Post(reqToken(), bind(api.CreateRepoOption{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetOrg), repo.CreateOrgRepo)
|
||||||
m.Group("/members", func() {
|
m.Group("/members", func() {
|
||||||
m.Get("", reqToken(), org.ListMembers)
|
m.Get("", reqToken(), org.ListMembers)
|
||||||
m.Combo("/{username}").Get(reqToken(), org.IsMember).
|
m.Combo("/{username}").Get(reqToken(), org.IsMember).
|
||||||
|
@ -1482,6 +1492,16 @@ func Routes() *web.Route {
|
||||||
}, reqToken(), reqOrgOwnership())
|
}, reqToken(), reqOrgOwnership())
|
||||||
m.Get("/activities/feeds", org.ListOrgActivityFeeds)
|
m.Get("/activities/feeds", org.ListOrgActivityFeeds)
|
||||||
|
|
||||||
|
if setting.Quota.Enabled {
|
||||||
|
m.Group("/quota", func() {
|
||||||
|
m.Get("", org.GetQuota)
|
||||||
|
m.Get("/check", org.CheckQuota)
|
||||||
|
m.Get("/attachments", org.ListQuotaAttachments)
|
||||||
|
m.Get("/packages", org.ListQuotaPackages)
|
||||||
|
m.Get("/artifacts", org.ListQuotaArtifacts)
|
||||||
|
}, reqToken(), reqOrgOwnership())
|
||||||
|
}
|
||||||
|
|
||||||
m.Group("", func() {
|
m.Group("", func() {
|
||||||
m.Get("/list_blocked", org.ListBlockedUsers)
|
m.Get("/list_blocked", org.ListBlockedUsers)
|
||||||
m.Group("", func() {
|
m.Group("", func() {
|
||||||
|
@ -1531,6 +1551,12 @@ func Routes() *web.Route {
|
||||||
m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg)
|
m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg)
|
||||||
m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
|
m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
|
||||||
m.Post("/rename", bind(api.RenameUserOption{}), admin.RenameUser)
|
m.Post("/rename", bind(api.RenameUserOption{}), admin.RenameUser)
|
||||||
|
if setting.Quota.Enabled {
|
||||||
|
m.Group("/quota", func() {
|
||||||
|
m.Get("", admin.GetUserQuota)
|
||||||
|
m.Post("/groups", bind(api.SetUserQuotaGroupsOptions{}), admin.SetUserQuotaGroups)
|
||||||
|
})
|
||||||
|
}
|
||||||
}, context.UserAssignmentAPI())
|
}, context.UserAssignmentAPI())
|
||||||
})
|
})
|
||||||
m.Group("/emails", func() {
|
m.Group("/emails", func() {
|
||||||
|
@ -1552,6 +1578,37 @@ func Routes() *web.Route {
|
||||||
m.Group("/runners", func() {
|
m.Group("/runners", func() {
|
||||||
m.Get("/registration-token", admin.GetRegistrationToken)
|
m.Get("/registration-token", admin.GetRegistrationToken)
|
||||||
})
|
})
|
||||||
|
if setting.Quota.Enabled {
|
||||||
|
m.Group("/quota", func() {
|
||||||
|
m.Group("/rules", func() {
|
||||||
|
m.Combo("").Get(admin.ListQuotaRules).
|
||||||
|
Post(bind(api.CreateQuotaRuleOptions{}), admin.CreateQuotaRule)
|
||||||
|
m.Combo("/{quotarule}", context.QuotaRuleAssignmentAPI()).
|
||||||
|
Get(admin.GetQuotaRule).
|
||||||
|
Patch(bind(api.EditQuotaRuleOptions{}), admin.EditQuotaRule).
|
||||||
|
Delete(admin.DeleteQuotaRule)
|
||||||
|
})
|
||||||
|
m.Group("/groups", func() {
|
||||||
|
m.Combo("").Get(admin.ListQuotaGroups).
|
||||||
|
Post(bind(api.CreateQuotaGroupOptions{}), admin.CreateQuotaGroup)
|
||||||
|
m.Group("/{quotagroup}", func() {
|
||||||
|
m.Combo("").Get(admin.GetQuotaGroup).
|
||||||
|
Delete(admin.DeleteQuotaGroup)
|
||||||
|
m.Group("/rules", func() {
|
||||||
|
m.Combo("/{quotarule}", context.QuotaRuleAssignmentAPI()).
|
||||||
|
Put(admin.AddRuleToQuotaGroup).
|
||||||
|
Delete(admin.RemoveRuleFromQuotaGroup)
|
||||||
|
})
|
||||||
|
m.Group("/users", func() {
|
||||||
|
m.Get("", admin.ListUsersInQuotaGroup)
|
||||||
|
m.Combo("/{username}", context.UserAssignmentAPI()).
|
||||||
|
Put(admin.AddUserToQuotaGroup).
|
||||||
|
Delete(admin.RemoveUserFromQuotaGroup)
|
||||||
|
})
|
||||||
|
}, context.QuotaGroupAssignmentAPI())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin())
|
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin())
|
||||||
|
|
||||||
m.Group("/topics", func() {
|
m.Group("/topics", func() {
|
||||||
|
|
155
routers/api/v1/org/quota.go
Normal file
155
routers/api/v1/org/quota.go
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package org
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.gitea.io/gitea/routers/api/v1/shared"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetQuota returns the quota information for a given organization
|
||||||
|
func GetQuota(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /orgs/{org}/quota organization orgGetQuota
|
||||||
|
// ---
|
||||||
|
// summary: Get quota information for an organization
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: org
|
||||||
|
// in: path
|
||||||
|
// description: name of the organization
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/QuotaInfo"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
shared.GetQuota(ctx, ctx.Org.Organization.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckQuota returns whether the organization in context is over the subject quota
|
||||||
|
func CheckQuota(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /orgs/{org}/quota/check organization orgCheckQuota
|
||||||
|
// ---
|
||||||
|
// summary: Check if the organization is over quota for a given subject
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: org
|
||||||
|
// in: path
|
||||||
|
// description: name of the organization
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/boolean"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
shared.CheckQuota(ctx, ctx.Org.Organization.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListQuotaAttachments lists attachments affecting the organization's quota
|
||||||
|
func ListQuotaAttachments(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /orgs/{org}/quota/attachments organization orgListQuotaAttachments
|
||||||
|
// ---
|
||||||
|
// summary: List the attachments affecting the organization's quota
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: org
|
||||||
|
// in: path
|
||||||
|
// description: name of the organization
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: page
|
||||||
|
// in: query
|
||||||
|
// description: page number of results to return (1-based)
|
||||||
|
// type: integer
|
||||||
|
// - name: limit
|
||||||
|
// in: query
|
||||||
|
// description: page size of results
|
||||||
|
// type: integer
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/QuotaUsedAttachmentList"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
shared.ListQuotaAttachments(ctx, ctx.Org.Organization.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListQuotaPackages lists packages affecting the organization's quota
|
||||||
|
func ListQuotaPackages(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /orgs/{org}/quota/packages organization orgListQuotaPackages
|
||||||
|
// ---
|
||||||
|
// summary: List the packages affecting the organization's quota
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: org
|
||||||
|
// in: path
|
||||||
|
// description: name of the organization
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: page
|
||||||
|
// in: query
|
||||||
|
// description: page number of results to return (1-based)
|
||||||
|
// type: integer
|
||||||
|
// - name: limit
|
||||||
|
// in: query
|
||||||
|
// description: page size of results
|
||||||
|
// type: integer
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/QuotaUsedPackageList"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
shared.ListQuotaPackages(ctx, ctx.Org.Organization.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListQuotaArtifacts lists artifacts affecting the organization's quota
|
||||||
|
func ListQuotaArtifacts(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /orgs/{org}/quota/artifacts organization orgListQuotaArtifacts
|
||||||
|
// ---
|
||||||
|
// summary: List the artifacts affecting the organization's quota
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: org
|
||||||
|
// in: path
|
||||||
|
// description: name of the organization
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: page
|
||||||
|
// in: query
|
||||||
|
// description: page number of results to return (1-based)
|
||||||
|
// type: integer
|
||||||
|
// - name: limit
|
||||||
|
// in: query
|
||||||
|
// description: page size of results
|
||||||
|
// type: integer
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/QuotaUsedArtifactList"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
shared.ListQuotaArtifacts(ctx, ctx.Org.Organization.ID)
|
||||||
|
}
|
|
@ -210,6 +210,8 @@ func CreateBranch(ctx *context.APIContext) {
|
||||||
// description: The old branch does not exist.
|
// description: The old branch does not exist.
|
||||||
// "409":
|
// "409":
|
||||||
// description: The branch with the same name already exists.
|
// description: The branch with the same name already exists.
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
// "423":
|
// "423":
|
||||||
// "$ref": "#/responses/repoArchivedError"
|
// "$ref": "#/responses/repoArchivedError"
|
||||||
|
|
||||||
|
|
|
@ -477,6 +477,8 @@ func ChangeFiles(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
// "422":
|
// "422":
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
// "423":
|
// "423":
|
||||||
|
@ -579,6 +581,8 @@ func CreateFile(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
// "422":
|
// "422":
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
// "423":
|
// "423":
|
||||||
|
@ -677,6 +681,8 @@ func UpdateFile(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
// "422":
|
// "422":
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
// "423":
|
// "423":
|
||||||
|
@ -842,6 +848,8 @@ func DeleteFile(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
// "423":
|
// "423":
|
||||||
// "$ref": "#/responses/repoArchivedError"
|
// "$ref": "#/responses/repoArchivedError"
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"code.gitea.io/gitea/models/organization"
|
"code.gitea.io/gitea/models/organization"
|
||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
@ -105,6 +106,8 @@ func CreateFork(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
// "409":
|
// "409":
|
||||||
// description: The repository with the same name already exists.
|
// description: The repository with the same name already exists.
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
// "422":
|
// "422":
|
||||||
// "$ref": "#/responses/validationError"
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
@ -134,6 +137,10 @@ func CreateFork(ctx *context.APIContext) {
|
||||||
forker = org.AsUser()
|
forker = org.AsUser()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, forker.ID, forker.Name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var name string
|
var name string
|
||||||
if form.Name == nil {
|
if form.Name == nil {
|
||||||
name = repo.Name
|
name = repo.Name
|
||||||
|
|
|
@ -160,6 +160,8 @@ func CreateIssueAttachment(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
// "422":
|
// "422":
|
||||||
// "$ref": "#/responses/validationError"
|
// "$ref": "#/responses/validationError"
|
||||||
// "423":
|
// "423":
|
||||||
|
@ -269,6 +271,8 @@ func EditIssueAttachment(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/Attachment"
|
// "$ref": "#/responses/Attachment"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
// "423":
|
// "423":
|
||||||
// "$ref": "#/responses/repoArchivedError"
|
// "$ref": "#/responses/repoArchivedError"
|
||||||
|
|
||||||
|
|
|
@ -157,6 +157,8 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
// "422":
|
// "422":
|
||||||
// "$ref": "#/responses/validationError"
|
// "$ref": "#/responses/validationError"
|
||||||
// "423":
|
// "423":
|
||||||
|
@ -274,6 +276,8 @@ func EditIssueCommentAttachment(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/Attachment"
|
// "$ref": "#/responses/Attachment"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
// "423":
|
// "423":
|
||||||
// "$ref": "#/responses/repoArchivedError"
|
// "$ref": "#/responses/repoArchivedError"
|
||||||
attach := getIssueCommentAttachmentSafeWrite(ctx)
|
attach := getIssueCommentAttachmentSafeWrite(ctx)
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"code.gitea.io/gitea/models/organization"
|
"code.gitea.io/gitea/models/organization"
|
||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/graceful"
|
"code.gitea.io/gitea/modules/graceful"
|
||||||
|
@ -54,6 +55,8 @@ func Migrate(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/forbidden"
|
// "$ref": "#/responses/forbidden"
|
||||||
// "409":
|
// "409":
|
||||||
// description: The repository with the same name already exists.
|
// description: The repository with the same name already exists.
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
// "422":
|
// "422":
|
||||||
// "$ref": "#/responses/validationError"
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
@ -85,6 +88,10 @@ func Migrate(ctx *context.APIContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, repoOwner.ID, repoOwner.Name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !ctx.Doer.IsAdmin {
|
if !ctx.Doer.IsAdmin {
|
||||||
if !repoOwner.IsOrganization() && ctx.Doer.ID != repoOwner.ID {
|
if !repoOwner.IsOrganization() && ctx.Doer.ID != repoOwner.ID {
|
||||||
ctx.Error(http.StatusForbidden, "", "Given user is not an organization.")
|
ctx.Error(http.StatusForbidden, "", "Given user is not an organization.")
|
||||||
|
|
|
@ -50,6 +50,8 @@ func MirrorSync(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/forbidden"
|
// "$ref": "#/responses/forbidden"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
|
|
||||||
repo := ctx.Repo.Repository
|
repo := ctx.Repo.Repository
|
||||||
|
|
||||||
|
@ -103,6 +105,8 @@ func PushMirrorSync(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/forbidden"
|
// "$ref": "#/responses/forbidden"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
|
|
||||||
if !setting.Mirror.Enabled {
|
if !setting.Mirror.Enabled {
|
||||||
ctx.Error(http.StatusBadRequest, "PushMirrorSync", "Mirror feature is disabled")
|
ctx.Error(http.StatusBadRequest, "PushMirrorSync", "Mirror feature is disabled")
|
||||||
|
@ -279,6 +283,8 @@ func AddPushMirror(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
|
|
||||||
if !setting.Mirror.Enabled {
|
if !setting.Mirror.Enabled {
|
||||||
ctx.Error(http.StatusBadRequest, "AddPushMirror", "Mirror feature is disabled")
|
ctx.Error(http.StatusBadRequest, "AddPushMirror", "Mirror feature is disabled")
|
||||||
|
|
|
@ -47,6 +47,8 @@ func ApplyDiffPatch(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/FileResponse"
|
// "$ref": "#/responses/FileResponse"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
// "423":
|
// "423":
|
||||||
// "$ref": "#/responses/repoArchivedError"
|
// "$ref": "#/responses/repoArchivedError"
|
||||||
apiOpts := web.GetForm(ctx).(*api.ApplyDiffPatchFileOptions)
|
apiOpts := web.GetForm(ctx).(*api.ApplyDiffPatchFileOptions)
|
||||||
|
|
|
@ -387,6 +387,8 @@ func CreatePullRequest(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
// "409":
|
// "409":
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
// "422":
|
// "422":
|
||||||
// "$ref": "#/responses/validationError"
|
// "$ref": "#/responses/validationError"
|
||||||
// "423":
|
// "423":
|
||||||
|
@ -857,6 +859,8 @@ func MergePullRequest(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/empty"
|
// "$ref": "#/responses/empty"
|
||||||
// "409":
|
// "409":
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
// "423":
|
// "423":
|
||||||
// "$ref": "#/responses/repoArchivedError"
|
// "$ref": "#/responses/repoArchivedError"
|
||||||
|
|
||||||
|
@ -1218,6 +1222,8 @@ func UpdatePullRequest(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
// "409":
|
// "409":
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
// "422":
|
// "422":
|
||||||
// "$ref": "#/responses/validationError"
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
|
|
@ -201,6 +201,8 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
|
|
||||||
// Check if attachments are enabled
|
// Check if attachments are enabled
|
||||||
if !setting.Attachment.Enabled {
|
if !setting.Attachment.Enabled {
|
||||||
|
@ -348,6 +350,8 @@ func EditReleaseAttachment(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/Attachment"
|
// "$ref": "#/responses/Attachment"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
|
|
||||||
form := web.GetForm(ctx).(*api.EditAttachmentOptions)
|
form := web.GetForm(ctx).(*api.EditAttachmentOptions)
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"code.gitea.io/gitea/models/organization"
|
"code.gitea.io/gitea/models/organization"
|
||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
unit_model "code.gitea.io/gitea/models/unit"
|
unit_model "code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
@ -302,6 +303,8 @@ func Create(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
// "409":
|
// "409":
|
||||||
// description: The repository with the same name already exists.
|
// description: The repository with the same name already exists.
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
// "422":
|
// "422":
|
||||||
// "$ref": "#/responses/validationError"
|
// "$ref": "#/responses/validationError"
|
||||||
opt := web.GetForm(ctx).(*api.CreateRepoOption)
|
opt := web.GetForm(ctx).(*api.CreateRepoOption)
|
||||||
|
@ -346,6 +349,8 @@ func Generate(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
// "409":
|
// "409":
|
||||||
// description: The repository with the same name already exists.
|
// description: The repository with the same name already exists.
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
// "422":
|
// "422":
|
||||||
// "$ref": "#/responses/validationError"
|
// "$ref": "#/responses/validationError"
|
||||||
form := web.GetForm(ctx).(*api.GenerateRepoOption)
|
form := web.GetForm(ctx).(*api.GenerateRepoOption)
|
||||||
|
@ -412,6 +417,10 @@ func Generate(ctx *context.APIContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, ctxUser.ID, ctxUser.Name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
repo, err := repo_service.GenerateRepository(ctx, ctx.Doer, ctxUser, ctx.Repo.Repository, opts)
|
repo, err := repo_service.GenerateRepository(ctx, ctx.Doer, ctxUser, ctx.Repo.Repository, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if repo_model.IsErrRepoAlreadyExist(err) {
|
if repo_model.IsErrRepoAlreadyExist(err) {
|
||||||
|
|
|
@ -208,6 +208,8 @@ func CreateTag(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/empty"
|
// "$ref": "#/responses/empty"
|
||||||
// "409":
|
// "409":
|
||||||
// "$ref": "#/responses/conflict"
|
// "$ref": "#/responses/conflict"
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
// "422":
|
// "422":
|
||||||
// "$ref": "#/responses/validationError"
|
// "$ref": "#/responses/validationError"
|
||||||
// "423":
|
// "423":
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"code.gitea.io/gitea/models/organization"
|
"code.gitea.io/gitea/models/organization"
|
||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
@ -53,6 +54,8 @@ func Transfer(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/forbidden"
|
// "$ref": "#/responses/forbidden"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
// "422":
|
// "422":
|
||||||
// "$ref": "#/responses/validationError"
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
@ -76,6 +79,10 @@ func Transfer(ctx *context.APIContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, newOwner.ID, newOwner.Name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var teams []*organization.Team
|
var teams []*organization.Team
|
||||||
if opts.TeamIDs != nil {
|
if opts.TeamIDs != nil {
|
||||||
if !newOwner.IsOrganization() {
|
if !newOwner.IsOrganization() {
|
||||||
|
@ -162,6 +169,8 @@ func AcceptTransfer(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/forbidden"
|
// "$ref": "#/responses/forbidden"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
|
|
||||||
err := acceptOrRejectRepoTransfer(ctx, true)
|
err := acceptOrRejectRepoTransfer(ctx, true)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
|
@ -233,6 +242,11 @@ func acceptOrRejectRepoTransfer(ctx *context.APIContext, accept bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if accept {
|
if accept {
|
||||||
|
recipient := repoTransfer.Recipient
|
||||||
|
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, recipient.ID, recipient.Name) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams)
|
return repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,8 @@ func NewWikiPage(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/forbidden"
|
// "$ref": "#/responses/forbidden"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
// "423":
|
// "423":
|
||||||
// "$ref": "#/responses/repoArchivedError"
|
// "$ref": "#/responses/repoArchivedError"
|
||||||
|
|
||||||
|
@ -131,6 +133,8 @@ func EditWikiPage(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/forbidden"
|
// "$ref": "#/responses/forbidden"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "413":
|
||||||
|
// "$ref": "#/responses/quotaExceeded"
|
||||||
// "423":
|
// "423":
|
||||||
// "$ref": "#/responses/repoArchivedError"
|
// "$ref": "#/responses/repoArchivedError"
|
||||||
|
|
||||||
|
|
102
routers/api/v1/shared/quota.go
Normal file
102
routers/api/v1/shared/quota.go
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
|
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
"code.gitea.io/gitea/services/convert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetQuota(ctx *context.APIContext, userID int64) {
|
||||||
|
used, err := quota_model.GetUsedForUser(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "quota_model.GetUsedForUser", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
groups, err := quota_model.GetGroupsForUser(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "quota_model.GetGroupsForUser", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := convert.ToQuotaInfo(used, groups, false)
|
||||||
|
ctx.JSON(http.StatusOK, &result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckQuota(ctx *context.APIContext, userID int64) {
|
||||||
|
subjectQuery := ctx.FormTrim("subject")
|
||||||
|
|
||||||
|
subject, err := quota_model.ParseLimitSubject(subjectQuery)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusUnprocessableEntity, "quota_model.ParseLimitSubject", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := quota_model.EvaluateForUser(ctx, userID, subject)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "quota_model.EvaluateForUser", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, &ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListQuotaAttachments(ctx *context.APIContext, userID int64) {
|
||||||
|
opts := utils.GetListOptions(ctx)
|
||||||
|
count, attachments, err := quota_model.GetQuotaAttachmentsForUser(ctx, userID, opts)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "GetQuotaAttachmentsForUser", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := convert.ToQuotaUsedAttachmentList(ctx, *attachments)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "convert.ToQuotaUsedAttachmentList", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.SetLinkHeader(int(count), opts.PageSize)
|
||||||
|
ctx.SetTotalCountHeader(count)
|
||||||
|
ctx.JSON(http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListQuotaPackages(ctx *context.APIContext, userID int64) {
|
||||||
|
opts := utils.GetListOptions(ctx)
|
||||||
|
count, packages, err := quota_model.GetQuotaPackagesForUser(ctx, userID, opts)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "GetQuotaPackagesForUser", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := convert.ToQuotaUsedPackageList(ctx, *packages)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "convert.ToQuotaUsedPackageList", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.SetLinkHeader(int(count), opts.PageSize)
|
||||||
|
ctx.SetTotalCountHeader(count)
|
||||||
|
ctx.JSON(http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListQuotaArtifacts(ctx *context.APIContext, userID int64) {
|
||||||
|
opts := utils.GetListOptions(ctx)
|
||||||
|
count, artifacts, err := quota_model.GetQuotaArtifactsForUser(ctx, userID, opts)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "GetQuotaArtifactsForUser", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := convert.ToQuotaUsedArtifactList(ctx, *artifacts)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "convert.ToQuotaUsedArtifactList", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.SetLinkHeader(int(count), opts.PageSize)
|
||||||
|
ctx.SetTotalCountHeader(count)
|
||||||
|
ctx.JSON(http.StatusOK, result)
|
||||||
|
}
|
|
@ -62,3 +62,10 @@ type swaggerResponseLabelTemplateInfo struct {
|
||||||
// in:body
|
// in:body
|
||||||
Body []api.LabelTemplate `json:"body"`
|
Body []api.LabelTemplate `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Boolean
|
||||||
|
// swagger:response boolean
|
||||||
|
type swaggerResponseBoolean struct {
|
||||||
|
// in:body
|
||||||
|
Body bool `json:"body"`
|
||||||
|
}
|
||||||
|
|
|
@ -219,4 +219,16 @@ type swaggerParameterBodies struct {
|
||||||
|
|
||||||
// in:body
|
// in:body
|
||||||
DispatchWorkflowOption api.DispatchWorkflowOption
|
DispatchWorkflowOption api.DispatchWorkflowOption
|
||||||
|
|
||||||
|
// in:body
|
||||||
|
CreateQuotaGroupOptions api.CreateQuotaGroupOptions
|
||||||
|
|
||||||
|
// in:body
|
||||||
|
CreateQuotaRuleOptions api.CreateQuotaRuleOptions
|
||||||
|
|
||||||
|
// in:body
|
||||||
|
EditQuotaRuleOptions api.EditQuotaRuleOptions
|
||||||
|
|
||||||
|
// in:body
|
||||||
|
SetUserQuotaGroupsOptions api.SetUserQuotaGroupsOptions
|
||||||
}
|
}
|
||||||
|
|
64
routers/api/v1/swagger/quota.go
Normal file
64
routers/api/v1/swagger/quota.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package swagger
|
||||||
|
|
||||||
|
import (
|
||||||
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// QuotaInfo
|
||||||
|
// swagger:response QuotaInfo
|
||||||
|
type swaggerResponseQuotaInfo struct {
|
||||||
|
// in:body
|
||||||
|
Body api.QuotaInfo `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaRuleInfoList
|
||||||
|
// swagger:response QuotaRuleInfoList
|
||||||
|
type swaggerResponseQuotaRuleInfoList struct {
|
||||||
|
// in:body
|
||||||
|
Body []api.QuotaRuleInfo `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaRuleInfo
|
||||||
|
// swagger:response QuotaRuleInfo
|
||||||
|
type swaggerResponseQuotaRuleInfo struct {
|
||||||
|
// in:body
|
||||||
|
Body api.QuotaRuleInfo `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaUsedAttachmentList
|
||||||
|
// swagger:response QuotaUsedAttachmentList
|
||||||
|
type swaggerQuotaUsedAttachmentList struct {
|
||||||
|
// in:body
|
||||||
|
Body api.QuotaUsedAttachmentList `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaUsedPackageList
|
||||||
|
// swagger:response QuotaUsedPackageList
|
||||||
|
type swaggerQuotaUsedPackageList struct {
|
||||||
|
// in:body
|
||||||
|
Body api.QuotaUsedPackageList `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaUsedArtifactList
|
||||||
|
// swagger:response QuotaUsedArtifactList
|
||||||
|
type swaggerQuotaUsedArtifactList struct {
|
||||||
|
// in:body
|
||||||
|
Body api.QuotaUsedArtifactList `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaGroup
|
||||||
|
// swagger:response QuotaGroup
|
||||||
|
type swaggerResponseQuotaGroup struct {
|
||||||
|
// in:body
|
||||||
|
Body api.QuotaGroup `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaGroupList
|
||||||
|
// swagger:response QuotaGroupList
|
||||||
|
type swaggerResponseQuotaGroupList struct {
|
||||||
|
// in:body
|
||||||
|
Body api.QuotaGroupList `json:"body"`
|
||||||
|
}
|
118
routers/api/v1/user/quota.go
Normal file
118
routers/api/v1/user/quota.go
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.gitea.io/gitea/routers/api/v1/shared"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetQuota returns the quota information for the authenticated user
|
||||||
|
func GetQuota(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /user/quota user userGetQuota
|
||||||
|
// ---
|
||||||
|
// summary: Get quota information for the authenticated user
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/QuotaInfo"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
|
||||||
|
shared.GetQuota(ctx, ctx.Doer.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckQuota returns whether the authenticated user is over the subject quota
|
||||||
|
func CheckQuota(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /user/quota/check user userCheckQuota
|
||||||
|
// ---
|
||||||
|
// summary: Check if the authenticated user is over quota for a given subject
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/boolean"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
shared.CheckQuota(ctx, ctx.Doer.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListQuotaAttachments lists attachments affecting the authenticated user's quota
|
||||||
|
func ListQuotaAttachments(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /user/quota/attachments user userListQuotaAttachments
|
||||||
|
// ---
|
||||||
|
// summary: List the attachments affecting the authenticated user's quota
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: page
|
||||||
|
// in: query
|
||||||
|
// description: page number of results to return (1-based)
|
||||||
|
// type: integer
|
||||||
|
// - name: limit
|
||||||
|
// in: query
|
||||||
|
// description: page size of results
|
||||||
|
// type: integer
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/QuotaUsedAttachmentList"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
|
||||||
|
shared.ListQuotaAttachments(ctx, ctx.Doer.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListQuotaPackages lists packages affecting the authenticated user's quota
|
||||||
|
func ListQuotaPackages(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /user/quota/packages user userListQuotaPackages
|
||||||
|
// ---
|
||||||
|
// summary: List the packages affecting the authenticated user's quota
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: page
|
||||||
|
// in: query
|
||||||
|
// description: page number of results to return (1-based)
|
||||||
|
// type: integer
|
||||||
|
// - name: limit
|
||||||
|
// in: query
|
||||||
|
// description: page size of results
|
||||||
|
// type: integer
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/QuotaUsedPackageList"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
|
||||||
|
shared.ListQuotaPackages(ctx, ctx.Doer.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListQuotaArtifacts lists artifacts affecting the authenticated user's quota
|
||||||
|
func ListQuotaArtifacts(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /user/quota/artifacts user userListQuotaArtifacts
|
||||||
|
// ---
|
||||||
|
// summary: List the artifacts affecting the authenticated user's quota
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: page
|
||||||
|
// in: query
|
||||||
|
// description: page number of results to return (1-based)
|
||||||
|
// type: integer
|
||||||
|
// - name: limit
|
||||||
|
// in: query
|
||||||
|
// description: page size of results
|
||||||
|
// type: integer
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/QuotaUsedArtifactList"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
|
||||||
|
shared.ListQuotaArtifacts(ctx, ctx.Doer.ID)
|
||||||
|
}
|
|
@ -99,9 +99,15 @@ func ListMyRepos(ctx *context.APIContext) {
|
||||||
// in: query
|
// in: query
|
||||||
// description: page size of results
|
// description: page size of results
|
||||||
// type: integer
|
// type: integer
|
||||||
|
// - name: order_by
|
||||||
|
// in: query
|
||||||
|
// description: order the repositories by name (default), id, or size
|
||||||
|
// type: string
|
||||||
// responses:
|
// responses:
|
||||||
// "200":
|
// "200":
|
||||||
// "$ref": "#/responses/RepositoryList"
|
// "$ref": "#/responses/RepositoryList"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
opts := &repo_model.SearchRepoOptions{
|
opts := &repo_model.SearchRepoOptions{
|
||||||
ListOptions: utils.GetListOptions(ctx),
|
ListOptions: utils.GetListOptions(ctx),
|
||||||
|
@ -110,6 +116,19 @@ func ListMyRepos(ctx *context.APIContext) {
|
||||||
Private: ctx.IsSigned,
|
Private: ctx.IsSigned,
|
||||||
IncludeDescription: true,
|
IncludeDescription: true,
|
||||||
}
|
}
|
||||||
|
orderBy := ctx.FormTrim("order_by")
|
||||||
|
switch orderBy {
|
||||||
|
case "name":
|
||||||
|
opts.OrderBy = "name ASC"
|
||||||
|
case "size":
|
||||||
|
opts.OrderBy = "size DESC"
|
||||||
|
case "id":
|
||||||
|
opts.OrderBy = "id ASC"
|
||||||
|
case "":
|
||||||
|
default:
|
||||||
|
ctx.Error(http.StatusUnprocessableEntity, "", "invalid order_by")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
repos, count, err := repo_model.SearchRepository(ctx, opts)
|
repos, count, err := repo_model.SearchRepository(ctx, opts)
|
||||||
|
|
|
@ -15,11 +15,13 @@ import (
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
perm_model "code.gitea.io/gitea/models/perm"
|
perm_model "code.gitea.io/gitea/models/perm"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/private"
|
"code.gitea.io/gitea/modules/private"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
gitea_context "code.gitea.io/gitea/services/context"
|
gitea_context "code.gitea.io/gitea/services/context"
|
||||||
pull_service "code.gitea.io/gitea/services/pull"
|
pull_service "code.gitea.io/gitea/services/pull"
|
||||||
|
@ -47,6 +49,8 @@ type preReceiveContext struct {
|
||||||
|
|
||||||
opts *private.HookOptions
|
opts *private.HookOptions
|
||||||
|
|
||||||
|
isOverQuota bool
|
||||||
|
|
||||||
branchName string
|
branchName string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,6 +144,36 @@ func (ctx *preReceiveContext) assertPushOptions() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx *preReceiveContext) checkQuota() error {
|
||||||
|
if !setting.Quota.Enabled {
|
||||||
|
ctx.isOverQuota = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ctx.loadPusherAndPermission() {
|
||||||
|
ctx.isOverQuota = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := quota_model.EvaluateForUser(ctx, ctx.PrivateContext.Repo.Repository.OwnerID, quota_model.LimitSubjectSizeReposAll)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("quota_model.EvaluateForUser: %v", err)
|
||||||
|
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||||
|
UserMsg: "Error checking user quota",
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.isOverQuota = !ok
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *preReceiveContext) quotaExceeded() {
|
||||||
|
ctx.JSON(http.StatusRequestEntityTooLarge, private.Response{
|
||||||
|
UserMsg: "Quota exceeded",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// HookPreReceive checks whether a individual commit is acceptable
|
// HookPreReceive checks whether a individual commit is acceptable
|
||||||
func HookPreReceive(ctx *gitea_context.PrivateContext) {
|
func HookPreReceive(ctx *gitea_context.PrivateContext) {
|
||||||
opts := web.GetForm(ctx).(*private.HookOptions)
|
opts := web.GetForm(ctx).(*private.HookOptions)
|
||||||
|
@ -156,6 +190,10 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
|
||||||
}
|
}
|
||||||
log.Trace("Git push options validation succeeded")
|
log.Trace("Git push options validation succeeded")
|
||||||
|
|
||||||
|
if err := ourCtx.checkQuota(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Iterate across the provided old commit IDs
|
// Iterate across the provided old commit IDs
|
||||||
for i := range opts.OldCommitIDs {
|
for i := range opts.OldCommitIDs {
|
||||||
oldCommitID := opts.OldCommitIDs[i]
|
oldCommitID := opts.OldCommitIDs[i]
|
||||||
|
@ -170,6 +208,10 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
|
||||||
case git.SupportProcReceive && refFullName.IsFor():
|
case git.SupportProcReceive && refFullName.IsFor():
|
||||||
preReceiveFor(ourCtx, oldCommitID, newCommitID, refFullName)
|
preReceiveFor(ourCtx, oldCommitID, newCommitID, refFullName)
|
||||||
default:
|
default:
|
||||||
|
if ourCtx.isOverQuota {
|
||||||
|
ourCtx.quotaExceeded()
|
||||||
|
return
|
||||||
|
}
|
||||||
ourCtx.AssertCanWriteCode()
|
ourCtx.AssertCanWriteCode()
|
||||||
}
|
}
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
|
@ -211,6 +253,11 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
|
||||||
|
|
||||||
// Allow pushes to non-protected branches
|
// Allow pushes to non-protected branches
|
||||||
if protectBranch == nil {
|
if protectBranch == nil {
|
||||||
|
// ...unless the user is over quota, and the operation is not a delete
|
||||||
|
if newCommitID != objectFormat.EmptyObjectID().String() && ctx.isOverQuota {
|
||||||
|
ctx.quotaExceeded()
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
protectBranch.Repo = repo
|
protectBranch.Repo = repo
|
||||||
|
@ -452,6 +499,15 @@ func preReceiveTag(ctx *preReceiveContext, oldCommitID, newCommitID string, refF
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the user is over quota, and the push isn't a tag deletion, deny it
|
||||||
|
if ctx.isOverQuota {
|
||||||
|
objectFormat := ctx.Repo.GetObjectFormat()
|
||||||
|
if newCommitID != objectFormat.EmptyObjectID().String() {
|
||||||
|
ctx.quotaExceeded()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func preReceiveFor(ctx *preReceiveContext, oldCommitID, newCommitID string, refFullName git.RefName) { //nolint:unparam
|
func preReceiveFor(ctx *preReceiveContext, oldCommitID, newCommitID string, refFullName git.RefName) { //nolint:unparam
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
admin_model "code.gitea.io/gitea/models/admin"
|
admin_model "code.gitea.io/gitea/models/admin"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
|
@ -170,6 +171,10 @@ func MigratePost(ctx *context.Context) {
|
||||||
|
|
||||||
tpl := base.TplName("repo/migrate/" + form.Service.Name())
|
tpl := base.TplName("repo/migrate/" + form.Service.Name())
|
||||||
|
|
||||||
|
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, ctxUser.ID, ctxUser.Name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if ctx.HasError() {
|
if ctx.HasError() {
|
||||||
ctx.HTML(http.StatusOK, tpl)
|
ctx.HTML(http.StatusOK, tpl)
|
||||||
return
|
return
|
||||||
|
@ -260,6 +265,25 @@ func setMigrationContextData(ctx *context.Context, serviceType structs.GitServic
|
||||||
}
|
}
|
||||||
|
|
||||||
func MigrateRetryPost(ctx *context.Context) {
|
func MigrateRetryPost(ctx *context.Context) {
|
||||||
|
ok, err := quota_model.EvaluateForUser(ctx, ctx.Repo.Repository.OwnerID, quota_model.LimitSubjectSizeReposAll)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("quota_model.EvaluateForUser: %v", err)
|
||||||
|
ctx.ServerError("quota_model.EvaluateForUser", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
if err := task.SetMigrateTaskMessage(ctx, ctx.Repo.Repository.ID, ctx.Locale.TrString("repo.settings.pull_mirror_sync_quota_exceeded")); err != nil {
|
||||||
|
log.Error("SetMigrateTaskMessage failed: %v", err)
|
||||||
|
ctx.ServerError("task.SetMigrateTaskMessage", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.JSON(http.StatusRequestEntityTooLarge, map[string]any{
|
||||||
|
"ok": false,
|
||||||
|
"error": ctx.Tr("repo.settings.pull_mirror_sync_quota_exceeded"),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := task.RetryMigrateTask(ctx, ctx.Repo.Repository.ID); err != nil {
|
if err := task.RetryMigrateTask(ctx, ctx.Repo.Repository.ID); err != nil {
|
||||||
log.Error("Retry task failed: %v", err)
|
log.Error("Retry task failed: %v", err)
|
||||||
ctx.ServerError("task.RetryMigrateTask", err)
|
ctx.ServerError("task.RetryMigrateTask", err)
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"code.gitea.io/gitea/models/organization"
|
"code.gitea.io/gitea/models/organization"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
pull_model "code.gitea.io/gitea/models/pull"
|
pull_model "code.gitea.io/gitea/models/pull"
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
@ -250,6 +251,10 @@ func ForkPost(ctx *context.Context) {
|
||||||
|
|
||||||
ctx.Data["ContextUser"] = ctxUser
|
ctx.Data["ContextUser"] = ctxUser
|
||||||
|
|
||||||
|
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, ctxUser.ID, ctxUser.Name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if ctx.HasError() {
|
if ctx.HasError() {
|
||||||
ctx.HTML(http.StatusOK, tplFork)
|
ctx.HTML(http.StatusOK, tplFork)
|
||||||
return
|
return
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
"code.gitea.io/gitea/models/organization"
|
"code.gitea.io/gitea/models/organization"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
@ -240,6 +241,10 @@ func CreatePost(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
ctx.Data["ContextUser"] = ctxUser
|
ctx.Data["ContextUser"] = ctxUser
|
||||||
|
|
||||||
|
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, ctxUser.ID, ctxUser.Name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if ctx.HasError() {
|
if ctx.HasError() {
|
||||||
ctx.HTML(http.StatusOK, tplCreate)
|
ctx.HTML(http.StatusOK, tplCreate)
|
||||||
return
|
return
|
||||||
|
@ -363,49 +368,56 @@ func ActionTransfer(accept bool) func(ctx *context.Context) {
|
||||||
action = "reject_transfer"
|
action = "reject_transfer"
|
||||||
}
|
}
|
||||||
|
|
||||||
err := acceptOrRejectRepoTransfer(ctx, accept)
|
ok, err := acceptOrRejectRepoTransfer(ctx, accept)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError(fmt.Sprintf("Action (%s)", action), err)
|
ctx.ServerError(fmt.Sprintf("Action (%s)", action), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ctx.RedirectToFirst(ctx.FormString("redirect_to"), ctx.Repo.RepoLink)
|
ctx.RedirectToFirst(ctx.FormString("redirect_to"), ctx.Repo.RepoLink)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error {
|
func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) (bool, error) {
|
||||||
repoTransfer, err := models.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
|
repoTransfer, err := models.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := repoTransfer.LoadAttributes(ctx); err != nil {
|
if err := repoTransfer.LoadAttributes(ctx); err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer) {
|
if !repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer) {
|
||||||
return errors.New("user does not have enough permissions")
|
return false, errors.New("user does not have enough permissions")
|
||||||
}
|
}
|
||||||
|
|
||||||
if accept {
|
if accept {
|
||||||
|
if !ctx.CheckQuota(quota_model.LimitSubjectSizeReposAll, ctx.Doer.ID, ctx.Doer.Name) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
if ctx.Repo.GitRepo != nil {
|
if ctx.Repo.GitRepo != nil {
|
||||||
ctx.Repo.GitRepo.Close()
|
ctx.Repo.GitRepo.Close()
|
||||||
ctx.Repo.GitRepo = nil
|
ctx.Repo.GitRepo = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams); err != nil {
|
if err := repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams); err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success"))
|
ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success"))
|
||||||
} else {
|
} else {
|
||||||
if err := repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository); err != nil {
|
if err := repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository); err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected"))
|
ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected"))
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Redirect(ctx.Repo.Repository.Link())
|
ctx.Redirect(ctx.Repo.Repository.Link())
|
||||||
return nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RedirectDownload return a file based on the following infos:
|
// RedirectDownload return a file based on the following infos:
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/organization"
|
"code.gitea.io/gitea/models/organization"
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
unit_model "code.gitea.io/gitea/models/unit"
|
unit_model "code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
@ -518,6 +519,20 @@ func SettingsPost(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ok, err := quota_model.EvaluateForUser(ctx, repo.OwnerID, quota_model.LimitSubjectSizeReposAll)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("quota_model.EvaluateForUser", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
// This section doesn't require repo_name/RepoName to be set in the form, don't show it
|
||||||
|
// as an error on the UI for this action
|
||||||
|
ctx.Data["Err_RepoName"] = nil
|
||||||
|
|
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.settings.pull_mirror_sync_quota_exceeded"), tplSettingsOptions, &form)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
mirror_service.AddPullMirrorToQueue(repo.ID)
|
mirror_service.AddPullMirrorToQueue(repo.ID)
|
||||||
|
|
||||||
ctx.Flash.Info(ctx.Tr("repo.settings.pull_mirror_sync_in_progress", repo.OriginalURL))
|
ctx.Flash.Info(ctx.Tr("repo.settings.pull_mirror_sync_in_progress", repo.OriginalURL))
|
||||||
|
@ -828,6 +843,17 @@ func SettingsPost(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check the quota of the new owner
|
||||||
|
ok, err := quota_model.EvaluateForUser(ctx, newOwner.ID, quota_model.LimitSubjectSizeReposAll)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("quota_model.EvaluateForUser", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_quota_exceeded", newOwner.Name), tplSettingsOptions, &form)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Close the GitRepo if open
|
// Close the GitRepo if open
|
||||||
if ctx.Repo.GitRepo != nil {
|
if ctx.Repo.GitRepo != nil {
|
||||||
ctx.Repo.GitRepo.Close()
|
ctx.Repo.GitRepo.Close()
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/metrics"
|
"code.gitea.io/gitea/modules/metrics"
|
||||||
|
@ -1196,7 +1197,7 @@ func registerRoutes(m *web.Route) {
|
||||||
m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus)
|
m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus)
|
||||||
m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues)
|
m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues)
|
||||||
m.Post("/resolve_conversation", reqRepoIssuesOrPullsReader, repo.SetShowOutdatedComments, repo.UpdateResolveConversation)
|
m.Post("/resolve_conversation", reqRepoIssuesOrPullsReader, repo.SetShowOutdatedComments, repo.UpdateResolveConversation)
|
||||||
m.Post("/attachments", repo.UploadIssueAttachment)
|
m.Post("/attachments", context.EnforceQuotaWeb(quota_model.LimitSubjectSizeAssetsAttachmentsIssues, context.QuotaTargetRepo), repo.UploadIssueAttachment)
|
||||||
m.Post("/attachments/remove", repo.DeleteAttachment)
|
m.Post("/attachments/remove", repo.DeleteAttachment)
|
||||||
m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin)
|
m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin)
|
||||||
m.Post("/move_pin", reqRepoAdmin, repo.IssuePinMove)
|
m.Post("/move_pin", reqRepoAdmin, repo.IssuePinMove)
|
||||||
|
@ -1244,9 +1245,9 @@ func registerRoutes(m *web.Route) {
|
||||||
Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost)
|
Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost)
|
||||||
m.Combo("/_cherrypick/{sha:([a-f0-9]{4,64})}/*").Get(repo.CherryPick).
|
m.Combo("/_cherrypick/{sha:([a-f0-9]{4,64})}/*").Get(repo.CherryPick).
|
||||||
Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost)
|
Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost)
|
||||||
}, repo.MustBeEditable, repo.CommonEditorData)
|
}, repo.MustBeEditable, repo.CommonEditorData, context.EnforceQuotaWeb(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo))
|
||||||
m.Group("", func() {
|
m.Group("", func() {
|
||||||
m.Post("/upload-file", repo.UploadFileToServer)
|
m.Post("/upload-file", context.EnforceQuotaWeb(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo), repo.UploadFileToServer)
|
||||||
m.Post("/upload-remove", web.Bind(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer)
|
m.Post("/upload-remove", web.Bind(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer)
|
||||||
}, repo.MustBeEditable, repo.MustBeAbleToUpload)
|
}, repo.MustBeEditable, repo.MustBeAbleToUpload)
|
||||||
}, context.RepoRef(), canEnableEditor, context.RepoMustNotBeArchived())
|
}, context.RepoRef(), canEnableEditor, context.RepoMustNotBeArchived())
|
||||||
|
@ -1256,7 +1257,7 @@ func registerRoutes(m *web.Route) {
|
||||||
m.Post("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.CreateBranch)
|
m.Post("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.CreateBranch)
|
||||||
m.Post("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.CreateBranch)
|
m.Post("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.CreateBranch)
|
||||||
m.Post("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.CreateBranch)
|
m.Post("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.CreateBranch)
|
||||||
}, web.Bind(forms.NewBranchForm{}))
|
}, web.Bind(forms.NewBranchForm{}), context.EnforceQuotaWeb(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo))
|
||||||
m.Post("/delete", repo.DeleteBranchPost)
|
m.Post("/delete", repo.DeleteBranchPost)
|
||||||
m.Post("/restore", repo.RestoreBranchPost)
|
m.Post("/restore", repo.RestoreBranchPost)
|
||||||
}, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty)
|
}, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty)
|
||||||
|
@ -1288,16 +1289,17 @@ func registerRoutes(m *web.Route) {
|
||||||
m.Get("/releases/attachments/{uuid}", repo.MustBeNotEmpty, repo.GetAttachment)
|
m.Get("/releases/attachments/{uuid}", repo.MustBeNotEmpty, repo.GetAttachment)
|
||||||
m.Get("/releases/download/{vTag}/{fileName}", repo.MustBeNotEmpty, repo.RedirectDownload)
|
m.Get("/releases/download/{vTag}/{fileName}", repo.MustBeNotEmpty, repo.RedirectDownload)
|
||||||
m.Group("/releases", func() {
|
m.Group("/releases", func() {
|
||||||
m.Get("/new", repo.NewRelease)
|
m.Combo("/new", context.EnforceQuotaWeb(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo)).
|
||||||
m.Post("/new", web.Bind(forms.NewReleaseForm{}), repo.NewReleasePost)
|
Get(repo.NewRelease).
|
||||||
|
Post(web.Bind(forms.NewReleaseForm{}), repo.NewReleasePost)
|
||||||
m.Post("/delete", repo.DeleteRelease)
|
m.Post("/delete", repo.DeleteRelease)
|
||||||
m.Post("/attachments", repo.UploadReleaseAttachment)
|
m.Post("/attachments", context.EnforceQuotaWeb(quota_model.LimitSubjectSizeAssetsAttachmentsReleases, context.QuotaTargetRepo), repo.UploadReleaseAttachment)
|
||||||
m.Post("/attachments/remove", repo.DeleteAttachment)
|
m.Post("/attachments/remove", repo.DeleteAttachment)
|
||||||
}, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, context.RepoRef())
|
}, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, context.RepoRef())
|
||||||
m.Group("/releases", func() {
|
m.Group("/releases", func() {
|
||||||
m.Get("/edit/*", repo.EditRelease)
|
m.Get("/edit/*", repo.EditRelease)
|
||||||
m.Post("/edit/*", web.Bind(forms.EditReleaseForm{}), repo.EditReleasePost)
|
m.Post("/edit/*", web.Bind(forms.EditReleaseForm{}), repo.EditReleasePost)
|
||||||
}, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, repo.CommitInfoCache)
|
}, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, repo.CommitInfoCache, context.EnforceQuotaWeb(quota_model.LimitSubjectSizeReposAll, context.QuotaTargetRepo))
|
||||||
}, ignSignIn, context.RepoAssignment, context.UnitTypes(), reqRepoReleaseReader)
|
}, ignSignIn, context.RepoAssignment, context.UnitTypes(), reqRepoReleaseReader)
|
||||||
|
|
||||||
// to maintain compatibility with old attachments
|
// to maintain compatibility with old attachments
|
||||||
|
@ -1410,10 +1412,10 @@ func registerRoutes(m *web.Route) {
|
||||||
m.Group("/wiki", func() {
|
m.Group("/wiki", func() {
|
||||||
m.Combo("/").
|
m.Combo("/").
|
||||||
Get(repo.Wiki).
|
Get(repo.Wiki).
|
||||||
Post(context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost)
|
Post(context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter, web.Bind(forms.NewWikiForm{}), context.EnforceQuotaWeb(quota_model.LimitSubjectSizeWiki, context.QuotaTargetRepo), repo.WikiPost)
|
||||||
m.Combo("/*").
|
m.Combo("/*").
|
||||||
Get(repo.Wiki).
|
Get(repo.Wiki).
|
||||||
Post(context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost)
|
Post(context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter, web.Bind(forms.NewWikiForm{}), context.EnforceQuotaWeb(quota_model.LimitSubjectSizeWiki, context.QuotaTargetRepo), repo.WikiPost)
|
||||||
m.Get("/commit/{sha:[a-f0-9]{4,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
|
m.Get("/commit/{sha:[a-f0-9]{4,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
|
||||||
m.Get("/commit/{sha:[a-f0-9]{4,64}}.{ext:patch|diff}", repo.RawDiff)
|
m.Get("/commit/{sha:[a-f0-9]{4,64}}.{ext:patch|diff}", repo.RawDiff)
|
||||||
}, repo.MustEnableWiki, func(ctx *context.Context) {
|
}, repo.MustEnableWiki, func(ctx *context.Context) {
|
||||||
|
@ -1490,7 +1492,7 @@ func registerRoutes(m *web.Route) {
|
||||||
m.Get("/list", context.RepoRef(), repo.GetPullCommits)
|
m.Get("/list", context.RepoRef(), repo.GetPullCommits)
|
||||||
m.Get("/{sha:[a-f0-9]{4,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit)
|
m.Get("/{sha:[a-f0-9]{4,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit)
|
||||||
})
|
})
|
||||||
m.Post("/merge", context.RepoMustNotBeArchived(), web.Bind(forms.MergePullRequestForm{}), repo.MergePullRequest)
|
m.Post("/merge", context.RepoMustNotBeArchived(), web.Bind(forms.MergePullRequestForm{}), context.EnforceQuotaWeb(quota_model.LimitSubjectSizeGitAll, context.QuotaTargetRepo), repo.MergePullRequest)
|
||||||
m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest)
|
m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest)
|
||||||
m.Post("/update", repo.UpdatePullRequest)
|
m.Post("/update", repo.UpdatePullRequest)
|
||||||
m.Post("/set_allow_maintainer_edit", web.Bind(forms.UpdateAllowEditsForm{}), repo.SetAllowEdits)
|
m.Post("/set_allow_maintainer_edit", web.Bind(forms.UpdateAllowEditsForm{}), repo.SetAllowEdits)
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
mc "code.gitea.io/gitea/modules/cache"
|
mc "code.gitea.io/gitea/modules/cache"
|
||||||
|
@ -42,6 +43,8 @@ type APIContext struct {
|
||||||
Comment *issues_model.Comment
|
Comment *issues_model.Comment
|
||||||
Org *APIOrganization
|
Org *APIOrganization
|
||||||
Package *Package
|
Package *Package
|
||||||
|
QuotaGroup *quota_model.Group
|
||||||
|
QuotaRule *quota_model.Rule
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
200
services/context/quota.go
Normal file
200
services/context/quota.go
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
|
"code.gitea.io/gitea/modules/base"
|
||||||
|
)
|
||||||
|
|
||||||
|
type QuotaTargetType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
QuotaTargetUser QuotaTargetType = iota
|
||||||
|
QuotaTargetRepo
|
||||||
|
QuotaTargetOrg
|
||||||
|
)
|
||||||
|
|
||||||
|
// QuotaExceeded
|
||||||
|
// swagger:response quotaExceeded
|
||||||
|
type APIQuotaExceeded struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
UserName string `json:"username,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaGroupAssignmentAPI returns a middleware to handle context-quota-group assignment for api routes
|
||||||
|
func QuotaGroupAssignmentAPI() func(ctx *APIContext) {
|
||||||
|
return func(ctx *APIContext) {
|
||||||
|
groupName := ctx.Params("quotagroup")
|
||||||
|
group, err := quota_model.GetGroupByName(ctx, groupName)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "quota_model.GetGroupByName", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if group == nil {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.QuotaGroup = group
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaRuleAssignmentAPI returns a middleware to handle context-quota-rule assignment for api routes
|
||||||
|
func QuotaRuleAssignmentAPI() func(ctx *APIContext) {
|
||||||
|
return func(ctx *APIContext) {
|
||||||
|
ruleName := ctx.Params("quotarule")
|
||||||
|
rule, err := quota_model.GetRuleByName(ctx, ruleName)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "quota_model.GetRuleByName", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if rule == nil {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.QuotaRule = rule
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ctx.CheckQuota checks whether the user in question is within quota limits (web context)
|
||||||
|
func (ctx *Context) CheckQuota(subject quota_model.LimitSubject, userID int64, username string) bool {
|
||||||
|
ok, err := checkQuota(ctx.Base.originCtx, subject, userID, username, func(userID int64, username string) {
|
||||||
|
showHTML := false
|
||||||
|
for _, part := range ctx.Req.Header["Accept"] {
|
||||||
|
if strings.Contains(part, "text/html") {
|
||||||
|
showHTML = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !showHTML {
|
||||||
|
ctx.plainTextInternal(3, http.StatusRequestEntityTooLarge, []byte("Quota exceeded.\n"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
|
||||||
|
ctx.Data["Title"] = "Quota Exceeded"
|
||||||
|
ctx.HTML(http.StatusRequestEntityTooLarge, base.TplName("status/413"))
|
||||||
|
}, func(err error) {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "quota_model.EvaluateForUser")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// ctx.CheckQuota checks whether the user in question is within quota limits (API context)
|
||||||
|
func (ctx *APIContext) CheckQuota(subject quota_model.LimitSubject, userID int64, username string) bool {
|
||||||
|
ok, err := checkQuota(ctx.Base.originCtx, subject, userID, username, func(userID int64, username string) {
|
||||||
|
ctx.JSON(http.StatusRequestEntityTooLarge, APIQuotaExceeded{
|
||||||
|
Message: "quota exceeded",
|
||||||
|
UserID: userID,
|
||||||
|
UserName: username,
|
||||||
|
})
|
||||||
|
}, func(err error) {
|
||||||
|
ctx.InternalServerError(err)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnforceQuotaWeb returns a middleware that enforces quota limits on the given web route.
|
||||||
|
func EnforceQuotaWeb(subject quota_model.LimitSubject, target QuotaTargetType) func(ctx *Context) {
|
||||||
|
return func(ctx *Context) {
|
||||||
|
ctx.CheckQuota(subject, target.UserID(ctx), target.UserName(ctx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnforceQuotaWeb returns a middleware that enforces quota limits on the given API route.
|
||||||
|
func EnforceQuotaAPI(subject quota_model.LimitSubject, target QuotaTargetType) func(ctx *APIContext) {
|
||||||
|
return func(ctx *APIContext) {
|
||||||
|
ctx.CheckQuota(subject, target.UserID(ctx), target.UserName(ctx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkQuota wraps quota checking into a single function
|
||||||
|
func checkQuota(ctx context.Context, subject quota_model.LimitSubject, userID int64, username string, quotaExceededHandler func(userID int64, username string), errorHandler func(err error)) (bool, error) {
|
||||||
|
ok, err := quota_model.EvaluateForUser(ctx, userID, subject)
|
||||||
|
if err != nil {
|
||||||
|
errorHandler(err)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
quotaExceededHandler(userID, username)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuotaContext interface {
|
||||||
|
GetQuotaTargetUserID(target QuotaTargetType) int64
|
||||||
|
GetQuotaTargetUserName(target QuotaTargetType) string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) GetQuotaTargetUserID(target QuotaTargetType) int64 {
|
||||||
|
switch target {
|
||||||
|
case QuotaTargetUser:
|
||||||
|
return ctx.Doer.ID
|
||||||
|
case QuotaTargetRepo:
|
||||||
|
return ctx.Repo.Repository.OwnerID
|
||||||
|
case QuotaTargetOrg:
|
||||||
|
return ctx.Org.Organization.ID
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) GetQuotaTargetUserName(target QuotaTargetType) string {
|
||||||
|
switch target {
|
||||||
|
case QuotaTargetUser:
|
||||||
|
return ctx.Doer.Name
|
||||||
|
case QuotaTargetRepo:
|
||||||
|
return ctx.Repo.Repository.Owner.Name
|
||||||
|
case QuotaTargetOrg:
|
||||||
|
return ctx.Org.Organization.Name
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *APIContext) GetQuotaTargetUserID(target QuotaTargetType) int64 {
|
||||||
|
switch target {
|
||||||
|
case QuotaTargetUser:
|
||||||
|
return ctx.Doer.ID
|
||||||
|
case QuotaTargetRepo:
|
||||||
|
return ctx.Repo.Repository.OwnerID
|
||||||
|
case QuotaTargetOrg:
|
||||||
|
return ctx.Org.Organization.ID
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *APIContext) GetQuotaTargetUserName(target QuotaTargetType) string {
|
||||||
|
switch target {
|
||||||
|
case QuotaTargetUser:
|
||||||
|
return ctx.Doer.Name
|
||||||
|
case QuotaTargetRepo:
|
||||||
|
return ctx.Repo.Repository.Owner.Name
|
||||||
|
case QuotaTargetOrg:
|
||||||
|
return ctx.Org.Organization.Name
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (target QuotaTargetType) UserID(ctx QuotaContext) int64 {
|
||||||
|
return ctx.GetQuotaTargetUserID(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (target QuotaTargetType) UserName(ctx QuotaContext) string {
|
||||||
|
return ctx.GetQuotaTargetUserName(target)
|
||||||
|
}
|
185
services/convert/quota.go
Normal file
185
services/convert/quota.go
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package convert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
action_model "code.gitea.io/gitea/models/actions"
|
||||||
|
issue_model "code.gitea.io/gitea/models/issues"
|
||||||
|
package_model "code.gitea.io/gitea/models/packages"
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ToQuotaRuleInfo(rule quota_model.Rule, withName bool) api.QuotaRuleInfo {
|
||||||
|
info := api.QuotaRuleInfo{
|
||||||
|
Limit: rule.Limit,
|
||||||
|
Subjects: make([]string, len(rule.Subjects)),
|
||||||
|
}
|
||||||
|
for i := range len(rule.Subjects) {
|
||||||
|
info.Subjects[i] = rule.Subjects[i].String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if withName {
|
||||||
|
info.Name = rule.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func toQuotaInfoUsed(used *quota_model.Used) api.QuotaUsed {
|
||||||
|
info := api.QuotaUsed{
|
||||||
|
Size: api.QuotaUsedSize{
|
||||||
|
Repos: api.QuotaUsedSizeRepos{
|
||||||
|
Public: used.Size.Repos.Public,
|
||||||
|
Private: used.Size.Repos.Private,
|
||||||
|
},
|
||||||
|
Git: api.QuotaUsedSizeGit{
|
||||||
|
LFS: used.Size.Git.LFS,
|
||||||
|
},
|
||||||
|
Assets: api.QuotaUsedSizeAssets{
|
||||||
|
Attachments: api.QuotaUsedSizeAssetsAttachments{
|
||||||
|
Issues: used.Size.Assets.Attachments.Issues,
|
||||||
|
Releases: used.Size.Assets.Attachments.Releases,
|
||||||
|
},
|
||||||
|
Artifacts: used.Size.Assets.Artifacts,
|
||||||
|
Packages: api.QuotaUsedSizeAssetsPackages{
|
||||||
|
All: used.Size.Assets.Packages.All,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToQuotaInfo(used *quota_model.Used, groups quota_model.GroupList, withNames bool) api.QuotaInfo {
|
||||||
|
info := api.QuotaInfo{
|
||||||
|
Used: toQuotaInfoUsed(used),
|
||||||
|
Groups: ToQuotaGroupList(groups, withNames),
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToQuotaGroup(group quota_model.Group, withNames bool) api.QuotaGroup {
|
||||||
|
info := api.QuotaGroup{
|
||||||
|
Rules: make([]api.QuotaRuleInfo, len(group.Rules)),
|
||||||
|
}
|
||||||
|
if withNames {
|
||||||
|
info.Name = group.Name
|
||||||
|
}
|
||||||
|
for i := range len(group.Rules) {
|
||||||
|
info.Rules[i] = ToQuotaRuleInfo(group.Rules[i], withNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToQuotaGroupList(groups quota_model.GroupList, withNames bool) api.QuotaGroupList {
|
||||||
|
list := make(api.QuotaGroupList, len(groups))
|
||||||
|
|
||||||
|
for i := range len(groups) {
|
||||||
|
list[i] = ToQuotaGroup(*groups[i], withNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToQuotaUsedAttachmentList(ctx context.Context, attachments []*repo_model.Attachment) (*api.QuotaUsedAttachmentList, error) {
|
||||||
|
getAttachmentContainer := func(a *repo_model.Attachment) (string, string, error) {
|
||||||
|
if a.ReleaseID != 0 {
|
||||||
|
release, err := repo_model.GetReleaseByID(ctx, a.ReleaseID)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
if err = release.LoadAttributes(ctx); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
return release.APIURL(), release.HTMLURL(), nil
|
||||||
|
}
|
||||||
|
if a.CommentID != 0 {
|
||||||
|
comment, err := issue_model.GetCommentByID(ctx, a.CommentID)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
return comment.APIURL(ctx), comment.HTMLURL(ctx), nil
|
||||||
|
}
|
||||||
|
if a.IssueID != 0 {
|
||||||
|
issue, err := issue_model.GetIssueByID(ctx, a.IssueID)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
if err = issue.LoadRepo(ctx); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
return issue.APIURL(ctx), issue.HTMLURL(), nil
|
||||||
|
}
|
||||||
|
return "", "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(api.QuotaUsedAttachmentList, len(attachments))
|
||||||
|
for i, a := range attachments {
|
||||||
|
capiURL, chtmlURL, err := getAttachmentContainer(a)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
apiURL := capiURL + "/assets/" + strconv.FormatInt(a.ID, 10)
|
||||||
|
result[i] = &api.QuotaUsedAttachment{
|
||||||
|
Name: a.Name,
|
||||||
|
Size: a.Size,
|
||||||
|
APIURL: apiURL,
|
||||||
|
}
|
||||||
|
result[i].ContainedIn.APIURL = capiURL
|
||||||
|
result[i].ContainedIn.HTMLURL = chtmlURL
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToQuotaUsedPackageList(ctx context.Context, packages []*package_model.PackageVersion) (*api.QuotaUsedPackageList, error) {
|
||||||
|
result := make(api.QuotaUsedPackageList, len(packages))
|
||||||
|
for i, pv := range packages {
|
||||||
|
d, err := package_model.GetPackageDescriptor(ctx, pv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var size int64
|
||||||
|
for _, file := range d.Files {
|
||||||
|
size += file.Blob.Size
|
||||||
|
}
|
||||||
|
|
||||||
|
result[i] = &api.QuotaUsedPackage{
|
||||||
|
Name: d.Package.Name,
|
||||||
|
Type: d.Package.Type.Name(),
|
||||||
|
Version: d.Version.Version,
|
||||||
|
Size: size,
|
||||||
|
HTMLURL: d.VersionHTMLURL(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToQuotaUsedArtifactList(ctx context.Context, artifacts []*action_model.ActionArtifact) (*api.QuotaUsedArtifactList, error) {
|
||||||
|
result := make(api.QuotaUsedArtifactList, len(artifacts))
|
||||||
|
for i, a := range artifacts {
|
||||||
|
run, err := action_model.GetRunByID(ctx, a.RunID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result[i] = &api.QuotaUsedArtifact{
|
||||||
|
Name: a.ArtifactName,
|
||||||
|
Size: a.FileCompressedSize,
|
||||||
|
HTMLURL: run.HTMLURL(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ import (
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
@ -179,6 +180,18 @@ func BatchHandler(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isUpload {
|
||||||
|
ok, err := quota_model.EvaluateForUser(ctx, ctx.Doer.ID, quota_model.LimitSubjectSizeGitLFS)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("quota_model.EvaluateForUser: %v", err)
|
||||||
|
writeStatus(ctx, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
writeStatusMessage(ctx, http.StatusRequestEntityTooLarge, "quota exceeded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
contentStore := lfs_module.NewContentStore()
|
contentStore := lfs_module.NewContentStore()
|
||||||
|
|
||||||
var responseObjects []*lfs_module.ObjectResponse
|
var responseObjects []*lfs_module.ObjectResponse
|
||||||
|
@ -297,6 +310,18 @@ func UploadHandler(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
ok, err := quota_model.EvaluateForUser(ctx, ctx.Doer.ID, quota_model.LimitSubjectSizeGitLFS)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("quota_model.EvaluateForUser: %v", err)
|
||||||
|
writeStatus(ctx, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
writeStatusMessage(ctx, http.StatusRequestEntityTooLarge, "quota exceeded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uploadOrVerify := func() error {
|
uploadOrVerify := func() error {
|
||||||
if exists {
|
if exists {
|
||||||
accessible, err := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid)
|
accessible, err := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid)
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/modules/graceful"
|
"code.gitea.io/gitea/modules/graceful"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
@ -73,6 +74,19 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error {
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the repo's owner is over quota, for pull mirrors
|
||||||
|
if mirrorType == PullMirrorType {
|
||||||
|
ok, err := quota_model.EvaluateForUser(ctx, repo.OwnerID, quota_model.LimitSubjectSizeReposAll)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("quota_model.EvaluateForUser: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
log.Trace("Owner quota exceeded for %-v, not syncing", repo)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Push to the Queue
|
// Push to the Queue
|
||||||
if err := PushToQueue(mirrorType, referenceID); err != nil {
|
if err := PushToQueue(mirrorType, referenceID); err != nil {
|
||||||
if err == queue.ErrAlreadyInQueue {
|
if err == queue.ErrAlreadyInQueue {
|
||||||
|
|
|
@ -152,3 +152,18 @@ func RetryMigrateTask(ctx context.Context, repoID int64) error {
|
||||||
|
|
||||||
return taskQueue.Push(migratingTask)
|
return taskQueue.Push(migratingTask)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetMigrateTaskMessage(ctx context.Context, repoID int64, message string) error {
|
||||||
|
migratingTask, err := admin_model.GetMigratingTask(ctx, repoID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("GetMigratingTask: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
migratingTask.Message = message
|
||||||
|
if err = migratingTask.UpdateCols(ctx, "message"); err != nil {
|
||||||
|
log.Error("task.UpdateCols failed: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
11
templates/status/413.tmpl
Normal file
11
templates/status/413.tmpl
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{{template "base/head" .}}
|
||||||
|
<div role="main" aria-label="{{.Title}}" class="page-content ui center tw-w-screen {{if .IsRepo}}repository{{end}}">
|
||||||
|
{{if .IsRepo}}{{template "repo/header" .}}{{end}}
|
||||||
|
<div class="ui container center">
|
||||||
|
<h1 style="margin-top: 100px" class="error-code">413</h1>
|
||||||
|
<p>{{ctx.Locale.Tr "error413"}}</p>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{template "base/footer" .}}
|
1477
templates/swagger/v1_json.tmpl
generated
1477
templates/swagger/v1_json.tmpl
generated
File diff suppressed because it is too large
Load diff
846
tests/integration/api_quota_management_test.go
Normal file
846
tests/integration/api_quota_management_test.go
Normal file
|
@ -0,0 +1,846 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
quota_model "code.gitea.io/gitea/models/quota"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
"code.gitea.io/gitea/routers"
|
||||||
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAPIQuotaDisabled(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
defer test.MockVariableValue(&setting.Quota.Enabled, false)()
|
||||||
|
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||||
|
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
|
||||||
|
session := loginUser(t, user.Name)
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/api/v1/user/quota")
|
||||||
|
session.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiCreateUser(t *testing.T, username string) func() {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
|
||||||
|
session := loginUser(t, admin.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
|
||||||
|
|
||||||
|
mustChangePassword := false
|
||||||
|
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/users", api.CreateUserOption{
|
||||||
|
Email: "api+" + username + "@example.com",
|
||||||
|
Username: username,
|
||||||
|
Password: "password",
|
||||||
|
MustChangePassword: &mustChangePassword,
|
||||||
|
}).AddTokenAuth(token)
|
||||||
|
session.MakeRequest(t, req, http.StatusCreated)
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
req := NewRequest(t, "DELETE", "/api/v1/admin/users/"+username+"?purge=true").AddTokenAuth(token)
|
||||||
|
session.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIQuotaCreateGroupWithRules(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
defer test.MockVariableValue(&setting.Quota.Enabled, true)()
|
||||||
|
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||||
|
|
||||||
|
// Create two rules in advance
|
||||||
|
unlimited := int64(-1)
|
||||||
|
defer createQuotaRule(t, api.CreateQuotaRuleOptions{
|
||||||
|
Name: "unlimited",
|
||||||
|
Limit: &unlimited,
|
||||||
|
Subjects: []string{"size:all"},
|
||||||
|
})()
|
||||||
|
zero := int64(0)
|
||||||
|
defer createQuotaRule(t, api.CreateQuotaRuleOptions{
|
||||||
|
Name: "deny-git-lfs",
|
||||||
|
Limit: &zero,
|
||||||
|
Subjects: []string{"size:git:lfs"},
|
||||||
|
})()
|
||||||
|
|
||||||
|
// Log in as admin
|
||||||
|
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
|
||||||
|
adminSession := loginUser(t, admin.Name)
|
||||||
|
adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll)
|
||||||
|
|
||||||
|
// Create a new group, with rules specified
|
||||||
|
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/groups", api.CreateQuotaGroupOptions{
|
||||||
|
Name: "group-with-rules",
|
||||||
|
Rules: []api.CreateQuotaRuleOptions{
|
||||||
|
// First: an existing group, unlimited, name only
|
||||||
|
{
|
||||||
|
Name: "unlimited",
|
||||||
|
},
|
||||||
|
// Second: an existing group, deny-git-lfs, with different params
|
||||||
|
{
|
||||||
|
Name: "deny-git-lfs",
|
||||||
|
Limit: &unlimited,
|
||||||
|
},
|
||||||
|
// Third: an entirely new group
|
||||||
|
{
|
||||||
|
Name: "new-rule",
|
||||||
|
Subjects: []string{"size:assets:all"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).AddTokenAuth(adminToken)
|
||||||
|
resp := adminSession.MakeRequest(t, req, http.StatusCreated)
|
||||||
|
defer func() {
|
||||||
|
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/group-with-rules").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
req = NewRequest(t, "DELETE", "/api/v1/admin/quota/rules/new-rule").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Verify that we created a group with rules included
|
||||||
|
var q api.QuotaGroup
|
||||||
|
DecodeJSON(t, resp, &q)
|
||||||
|
|
||||||
|
assert.Equal(t, "group-with-rules", q.Name)
|
||||||
|
assert.Len(t, q.Rules, 3)
|
||||||
|
|
||||||
|
// Verify that the previously existing rules are unchanged
|
||||||
|
rule, err := quota_model.GetRuleByName(db.DefaultContext, "unlimited")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, rule)
|
||||||
|
assert.EqualValues(t, -1, rule.Limit)
|
||||||
|
assert.EqualValues(t, quota_model.LimitSubjects{quota_model.LimitSubjectSizeAll}, rule.Subjects)
|
||||||
|
|
||||||
|
rule, err = quota_model.GetRuleByName(db.DefaultContext, "deny-git-lfs")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, rule)
|
||||||
|
assert.EqualValues(t, 0, rule.Limit)
|
||||||
|
assert.EqualValues(t, quota_model.LimitSubjects{quota_model.LimitSubjectSizeGitLFS}, rule.Subjects)
|
||||||
|
|
||||||
|
// Verify that the new rule was also created
|
||||||
|
rule, err = quota_model.GetRuleByName(db.DefaultContext, "new-rule")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, rule)
|
||||||
|
assert.EqualValues(t, 0, rule.Limit)
|
||||||
|
assert.EqualValues(t, quota_model.LimitSubjects{quota_model.LimitSubjectSizeAssetsAll}, rule.Subjects)
|
||||||
|
|
||||||
|
t.Run("invalid rule spec", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/groups", api.CreateQuotaGroupOptions{
|
||||||
|
Name: "group-with-invalid-rule-spec",
|
||||||
|
Rules: []api.CreateQuotaRuleOptions{
|
||||||
|
{
|
||||||
|
Name: "rule-with-wrong-spec",
|
||||||
|
Subjects: []string{"valid:false"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIQuotaEmptyState(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
defer test.MockVariableValue(&setting.Quota.Enabled, true)()
|
||||||
|
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||||
|
|
||||||
|
username := "quota-empty-user"
|
||||||
|
defer apiCreateUser(t, username)()
|
||||||
|
session := loginUser(t, username)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
|
||||||
|
|
||||||
|
t.Run("#/admin/users/quota-empty-user/quota", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
|
||||||
|
adminSession := loginUser(t, admin.Name)
|
||||||
|
adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll)
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/api/v1/admin/users/quota-empty-user/quota").AddTokenAuth(adminToken)
|
||||||
|
resp := adminSession.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
var q api.QuotaInfo
|
||||||
|
DecodeJSON(t, resp, &q)
|
||||||
|
|
||||||
|
assert.EqualValues(t, api.QuotaUsed{}, q.Used)
|
||||||
|
assert.Empty(t, q.Groups)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("#/user/quota", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/api/v1/user/quota").AddTokenAuth(token)
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
var q api.QuotaInfo
|
||||||
|
DecodeJSON(t, resp, &q)
|
||||||
|
|
||||||
|
assert.EqualValues(t, api.QuotaUsed{}, q.Used)
|
||||||
|
assert.Empty(t, q.Groups)
|
||||||
|
|
||||||
|
t.Run("#/user/quota/artifacts", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/api/v1/user/quota/artifacts").AddTokenAuth(token)
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
var q api.QuotaUsedArtifactList
|
||||||
|
DecodeJSON(t, resp, &q)
|
||||||
|
|
||||||
|
assert.Empty(t, q)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("#/user/quota/attachments", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/api/v1/user/quota/attachments").AddTokenAuth(token)
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
var q api.QuotaUsedAttachmentList
|
||||||
|
DecodeJSON(t, resp, &q)
|
||||||
|
|
||||||
|
assert.Empty(t, q)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("#/user/quota/packages", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/api/v1/user/quota/packages").AddTokenAuth(token)
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
var q api.QuotaUsedPackageList
|
||||||
|
DecodeJSON(t, resp, &q)
|
||||||
|
|
||||||
|
assert.Empty(t, q)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func createQuotaRule(t *testing.T, opts api.CreateQuotaRuleOptions) func() {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
|
||||||
|
adminSession := loginUser(t, admin.Name)
|
||||||
|
adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll)
|
||||||
|
|
||||||
|
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/rules", opts).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusCreated)
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
req := NewRequestf(t, "DELETE", "/api/v1/admin/quota/rules/%s", opts.Name).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createQuotaGroup(t *testing.T, name string) func() {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
|
||||||
|
adminSession := loginUser(t, admin.Name)
|
||||||
|
adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll)
|
||||||
|
|
||||||
|
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/groups", api.CreateQuotaGroupOptions{
|
||||||
|
Name: name,
|
||||||
|
}).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusCreated)
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
req := NewRequestf(t, "DELETE", "/api/v1/admin/quota/groups/%s", name).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIQuotaAdminRoutesRules(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
defer test.MockVariableValue(&setting.Quota.Enabled, true)()
|
||||||
|
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||||
|
|
||||||
|
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
|
||||||
|
adminSession := loginUser(t, admin.Name)
|
||||||
|
adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll)
|
||||||
|
|
||||||
|
zero := int64(0)
|
||||||
|
oneKb := int64(1024)
|
||||||
|
|
||||||
|
t.Run("adminCreateQuotaRule", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/rules", api.CreateQuotaRuleOptions{
|
||||||
|
Name: "deny-all",
|
||||||
|
Limit: &zero,
|
||||||
|
Subjects: []string{"size:all"},
|
||||||
|
}).AddTokenAuth(adminToken)
|
||||||
|
resp := adminSession.MakeRequest(t, req, http.StatusCreated)
|
||||||
|
defer func() {
|
||||||
|
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/rules/deny-all").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var q api.QuotaRuleInfo
|
||||||
|
DecodeJSON(t, resp, &q)
|
||||||
|
|
||||||
|
assert.Equal(t, "deny-all", q.Name)
|
||||||
|
assert.EqualValues(t, 0, q.Limit)
|
||||||
|
assert.EqualValues(t, []string{"size:all"}, q.Subjects)
|
||||||
|
|
||||||
|
rule, err := quota_model.GetRuleByName(db.DefaultContext, "deny-all")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.EqualValues(t, 0, rule.Limit)
|
||||||
|
|
||||||
|
t.Run("unhappy path", func(t *testing.T) {
|
||||||
|
t.Run("missing options", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/rules", nil).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid subjects", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/rules", api.CreateQuotaRuleOptions{
|
||||||
|
Name: "invalid-subjects",
|
||||||
|
Limit: &zero,
|
||||||
|
Subjects: []string{"valid:false"},
|
||||||
|
}).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("trying to add an existing rule", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
rule := api.CreateQuotaRuleOptions{
|
||||||
|
Name: "double-rule",
|
||||||
|
Limit: &zero,
|
||||||
|
}
|
||||||
|
|
||||||
|
defer createQuotaRule(t, rule)()
|
||||||
|
|
||||||
|
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/rules", rule).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusConflict)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adminDeleteQuotaRule", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
createQuotaRule(t, api.CreateQuotaRuleOptions{
|
||||||
|
Name: "deny-all",
|
||||||
|
Limit: &zero,
|
||||||
|
Subjects: []string{"size:all"},
|
||||||
|
})
|
||||||
|
|
||||||
|
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/rules/deny-all").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
rule, err := quota_model.GetRuleByName(db.DefaultContext, "deny-all")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, rule)
|
||||||
|
|
||||||
|
t.Run("unhappy path", func(t *testing.T) {
|
||||||
|
t.Run("nonexistent rule", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/rules/does-not-exist").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adminEditQuotaRule", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
defer createQuotaRule(t, api.CreateQuotaRuleOptions{
|
||||||
|
Name: "deny-all",
|
||||||
|
Limit: &zero,
|
||||||
|
Subjects: []string{"size:all"},
|
||||||
|
})()
|
||||||
|
|
||||||
|
req := NewRequestWithJSON(t, "PATCH", "/api/v1/admin/quota/rules/deny-all", api.EditQuotaRuleOptions{
|
||||||
|
Limit: &oneKb,
|
||||||
|
}).AddTokenAuth(adminToken)
|
||||||
|
resp := adminSession.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
var q api.QuotaRuleInfo
|
||||||
|
DecodeJSON(t, resp, &q)
|
||||||
|
assert.EqualValues(t, 1024, q.Limit)
|
||||||
|
|
||||||
|
rule, err := quota_model.GetRuleByName(db.DefaultContext, "deny-all")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.EqualValues(t, 1024, rule.Limit)
|
||||||
|
|
||||||
|
t.Run("no options", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequestWithJSON(t, "PATCH", "/api/v1/admin/quota/rules/deny-all", nil).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unhappy path", func(t *testing.T) {
|
||||||
|
t.Run("nonexistent rule", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequestWithJSON(t, "PATCH", "/api/v1/admin/quota/rules/does-not-exist", api.EditQuotaRuleOptions{
|
||||||
|
Limit: &oneKb,
|
||||||
|
}).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid subjects", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequestWithJSON(t, "PATCH", "/api/v1/admin/quota/rules/deny-all", api.EditQuotaRuleOptions{
|
||||||
|
Subjects: &[]string{"valid:false"},
|
||||||
|
}).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adminListQuotaRules", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
defer createQuotaRule(t, api.CreateQuotaRuleOptions{
|
||||||
|
Name: "deny-all",
|
||||||
|
Limit: &zero,
|
||||||
|
Subjects: []string{"size:all"},
|
||||||
|
})()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/api/v1/admin/quota/rules").AddTokenAuth(adminToken)
|
||||||
|
resp := adminSession.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
var rules []api.QuotaRuleInfo
|
||||||
|
DecodeJSON(t, resp, &rules)
|
||||||
|
|
||||||
|
assert.Len(t, rules, 1)
|
||||||
|
assert.Equal(t, "deny-all", rules[0].Name)
|
||||||
|
assert.EqualValues(t, 0, rules[0].Limit)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIQuotaAdminRoutesGroups(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
defer test.MockVariableValue(&setting.Quota.Enabled, true)()
|
||||||
|
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||||
|
|
||||||
|
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
|
||||||
|
adminSession := loginUser(t, admin.Name)
|
||||||
|
adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll)
|
||||||
|
|
||||||
|
zero := int64(0)
|
||||||
|
|
||||||
|
ruleDenyAll := api.CreateQuotaRuleOptions{
|
||||||
|
Name: "deny-all",
|
||||||
|
Limit: &zero,
|
||||||
|
Subjects: []string{"size:all"},
|
||||||
|
}
|
||||||
|
|
||||||
|
username := "quota-test-user"
|
||||||
|
defer apiCreateUser(t, username)()
|
||||||
|
|
||||||
|
t.Run("adminCreateQuotaGroup", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/groups", api.CreateQuotaGroupOptions{
|
||||||
|
Name: "default",
|
||||||
|
}).AddTokenAuth(adminToken)
|
||||||
|
resp := adminSession.MakeRequest(t, req, http.StatusCreated)
|
||||||
|
defer func() {
|
||||||
|
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/default").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var q api.QuotaGroup
|
||||||
|
DecodeJSON(t, resp, &q)
|
||||||
|
|
||||||
|
assert.Equal(t, "default", q.Name)
|
||||||
|
assert.Empty(t, q.Rules)
|
||||||
|
|
||||||
|
group, err := quota_model.GetGroupByName(db.DefaultContext, "default")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "default", group.Name)
|
||||||
|
assert.Empty(t, group.Rules)
|
||||||
|
|
||||||
|
t.Run("unhappy path", func(t *testing.T) {
|
||||||
|
t.Run("missing options", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/groups", nil).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("trying to add an existing group", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
defer createQuotaGroup(t, "duplicate")()
|
||||||
|
|
||||||
|
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/groups", api.CreateQuotaGroupOptions{
|
||||||
|
Name: "duplicate",
|
||||||
|
}).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusConflict)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adminDeleteQuotaGroup", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
createQuotaGroup(t, "default")
|
||||||
|
|
||||||
|
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/default").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
group, err := quota_model.GetGroupByName(db.DefaultContext, "default")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, group)
|
||||||
|
|
||||||
|
t.Run("unhappy path", func(t *testing.T) {
|
||||||
|
t.Run("non-existing group", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/does-not-exist").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adminAddRuleToQuotaGroup", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
defer createQuotaGroup(t, "default")()
|
||||||
|
defer createQuotaRule(t, ruleDenyAll)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/rules/deny-all").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
group, err := quota_model.GetGroupByName(db.DefaultContext, "default")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, group.Rules, 1)
|
||||||
|
assert.Equal(t, "deny-all", group.Rules[0].Name)
|
||||||
|
|
||||||
|
t.Run("unhappy path", func(t *testing.T) {
|
||||||
|
t.Run("non-existing group", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/does-not-exist/rules/deny-all").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non-existing rule", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/rules/does-not-exist").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adminRemoveRuleFromQuotaGroup", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
defer createQuotaGroup(t, "default")()
|
||||||
|
defer createQuotaRule(t, ruleDenyAll)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/rules/deny-all").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
req = NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/default/rules/deny-all").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
group, err := quota_model.GetGroupByName(db.DefaultContext, "default")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "default", group.Name)
|
||||||
|
assert.Empty(t, group.Rules)
|
||||||
|
|
||||||
|
t.Run("unhappy path", func(t *testing.T) {
|
||||||
|
t.Run("non-existing group", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/does-not-exist/rules/deny-all").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non-existing rule", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/default/rules/does-not-exist").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("rule not in group", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
defer createQuotaRule(t, api.CreateQuotaRuleOptions{
|
||||||
|
Name: "rule-not-in-group",
|
||||||
|
Limit: &zero,
|
||||||
|
})()
|
||||||
|
|
||||||
|
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/default/rules/rule-not-in-group").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adminGetQuotaGroup", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
defer createQuotaGroup(t, "default")()
|
||||||
|
defer createQuotaRule(t, ruleDenyAll)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/rules/deny-all").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
req = NewRequest(t, "GET", "/api/v1/admin/quota/groups/default").AddTokenAuth(adminToken)
|
||||||
|
resp := adminSession.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
var q api.QuotaGroup
|
||||||
|
DecodeJSON(t, resp, &q)
|
||||||
|
|
||||||
|
assert.Equal(t, "default", q.Name)
|
||||||
|
assert.Len(t, q.Rules, 1)
|
||||||
|
assert.Equal(t, "deny-all", q.Rules[0].Name)
|
||||||
|
|
||||||
|
t.Run("unhappy path", func(t *testing.T) {
|
||||||
|
t.Run("non-existing group", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/api/v1/admin/quota/groups/does-not-exist").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adminListQuotaGroups", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
defer createQuotaGroup(t, "default")()
|
||||||
|
defer createQuotaRule(t, ruleDenyAll)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/rules/deny-all").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
req = NewRequest(t, "GET", "/api/v1/admin/quota/groups").AddTokenAuth(adminToken)
|
||||||
|
resp := adminSession.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
var q api.QuotaGroupList
|
||||||
|
DecodeJSON(t, resp, &q)
|
||||||
|
|
||||||
|
assert.Len(t, q, 1)
|
||||||
|
assert.Equal(t, "default", q[0].Name)
|
||||||
|
assert.Len(t, q[0].Rules, 1)
|
||||||
|
assert.Equal(t, "deny-all", q[0].Rules[0].Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adminAddUserToQuotaGroup", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
defer createQuotaGroup(t, "default")()
|
||||||
|
|
||||||
|
req := NewRequestf(t, "PUT", "/api/v1/admin/quota/groups/default/users/%s", username).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username})
|
||||||
|
|
||||||
|
groups, err := quota_model.GetGroupsForUser(db.DefaultContext, user.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, groups, 1)
|
||||||
|
assert.Equal(t, "default", groups[0].Name)
|
||||||
|
|
||||||
|
t.Run("unhappy path", func(t *testing.T) {
|
||||||
|
t.Run("non-existing group", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequestf(t, "PUT", "/api/v1/admin/quota/groups/does-not-exist/users/%s", username).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non-existing user", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/users/this-user-does-not-exist").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("user already added", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/users/user1").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
req = NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/users/user1").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusConflict)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adminRemoveUserFromQuotaGroup", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
defer createQuotaGroup(t, "default")()
|
||||||
|
|
||||||
|
req := NewRequestf(t, "PUT", "/api/v1/admin/quota/groups/default/users/%s", username).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
req = NewRequestf(t, "DELETE", "/api/v1/admin/quota/groups/default/users/%s", username).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username})
|
||||||
|
groups, err := quota_model.GetGroupsForUser(db.DefaultContext, user.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, groups)
|
||||||
|
|
||||||
|
t.Run("unhappy path", func(t *testing.T) {
|
||||||
|
t.Run("non-existing group", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequestf(t, "DELETE", "/api/v1/admin/quota/groups/does-not-exist/users/%s", username).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non-existing user", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/default/users/does-not-exist").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("user not in group", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/default/users/user1").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adminListUsersInQuotaGroup", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
defer createQuotaGroup(t, "default")()
|
||||||
|
|
||||||
|
req := NewRequestf(t, "PUT", "/api/v1/admin/quota/groups/default/users/%s", username).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
req = NewRequest(t, "GET", "/api/v1/admin/quota/groups/default/users").AddTokenAuth(adminToken)
|
||||||
|
resp := adminSession.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
var q []api.User
|
||||||
|
DecodeJSON(t, resp, &q)
|
||||||
|
|
||||||
|
assert.Len(t, q, 1)
|
||||||
|
assert.Equal(t, username, q[0].UserName)
|
||||||
|
|
||||||
|
t.Run("unhappy path", func(t *testing.T) {
|
||||||
|
t.Run("non-existing group", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/api/v1/admin/quota/groups/does-not-exist/users").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adminSetUserQuotaGroups", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
defer createQuotaGroup(t, "default")()
|
||||||
|
defer createQuotaGroup(t, "test-1")()
|
||||||
|
defer createQuotaGroup(t, "test-2")()
|
||||||
|
|
||||||
|
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/admin/users/%s/quota/groups", username), api.SetUserQuotaGroupsOptions{
|
||||||
|
Groups: &[]string{"default", "test-1", "test-2"},
|
||||||
|
}).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username})
|
||||||
|
|
||||||
|
groups, err := quota_model.GetGroupsForUser(db.DefaultContext, user.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, groups, 3)
|
||||||
|
|
||||||
|
t.Run("unhappy path", func(t *testing.T) {
|
||||||
|
t.Run("non-existing user", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/users/does-not-exist/quota/groups", api.SetUserQuotaGroupsOptions{
|
||||||
|
Groups: &[]string{"default", "test-1", "test-2"},
|
||||||
|
}).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non-existing group", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/admin/users/%s/quota/groups", username), api.SetUserQuotaGroupsOptions{
|
||||||
|
Groups: &[]string{"default", "test-1", "test-2", "this-group-does-not-exist"},
|
||||||
|
}).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIQuotaUserRoutes(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
defer test.MockVariableValue(&setting.Quota.Enabled, true)()
|
||||||
|
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||||
|
|
||||||
|
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
|
||||||
|
adminSession := loginUser(t, admin.Name)
|
||||||
|
adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll)
|
||||||
|
|
||||||
|
// Create a test user
|
||||||
|
username := "quota-test-user-routes"
|
||||||
|
defer apiCreateUser(t, username)()
|
||||||
|
session := loginUser(t, username)
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
|
||||||
|
|
||||||
|
// Set up rules & groups for the user
|
||||||
|
defer createQuotaGroup(t, "user-routes-deny")()
|
||||||
|
defer createQuotaGroup(t, "user-routes-1kb")()
|
||||||
|
|
||||||
|
zero := int64(0)
|
||||||
|
ruleDenyAll := api.CreateQuotaRuleOptions{
|
||||||
|
Name: "user-routes-deny-all",
|
||||||
|
Limit: &zero,
|
||||||
|
Subjects: []string{"size:all"},
|
||||||
|
}
|
||||||
|
defer createQuotaRule(t, ruleDenyAll)()
|
||||||
|
oneKb := int64(1024)
|
||||||
|
rule1KbStuff := api.CreateQuotaRuleOptions{
|
||||||
|
Name: "user-routes-1kb",
|
||||||
|
Limit: &oneKb,
|
||||||
|
Subjects: []string{"size:assets:attachments:releases", "size:assets:packages:all", "size:git:lfs"},
|
||||||
|
}
|
||||||
|
defer createQuotaRule(t, rule1KbStuff)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/user-routes-deny/rules/user-routes-deny-all").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
req = NewRequest(t, "PUT", "/api/v1/admin/quota/groups/user-routes-1kb/rules/user-routes-1kb").AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
req = NewRequestf(t, "PUT", "/api/v1/admin/quota/groups/user-routes-deny/users/%s", username).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
req = NewRequestf(t, "PUT", "/api/v1/admin/quota/groups/user-routes-1kb/users/%s", username).AddTokenAuth(adminToken)
|
||||||
|
adminSession.MakeRequest(t, req, http.StatusNoContent)
|
||||||
|
|
||||||
|
t.Run("userGetQuota", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", "/api/v1/user/quota").AddTokenAuth(token)
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
var q api.QuotaInfo
|
||||||
|
DecodeJSON(t, resp, &q)
|
||||||
|
|
||||||
|
assert.Len(t, q.Groups, 2)
|
||||||
|
assert.Len(t, q.Groups[0].Rules, 1)
|
||||||
|
assert.Len(t, q.Groups[1].Rules, 1)
|
||||||
|
})
|
||||||
|
}
|
1436
tests/integration/api_quota_use_test.go
Normal file
1436
tests/integration/api_quota_use_test.go
Normal file
File diff suppressed because it is too large
Load diff
|
@ -701,6 +701,7 @@ type DeclarativeRepoOptions struct {
|
||||||
Files optional.Option[[]*files_service.ChangeRepoFile]
|
Files optional.Option[[]*files_service.ChangeRepoFile]
|
||||||
WikiBranch optional.Option[string]
|
WikiBranch optional.Option[string]
|
||||||
AutoInit optional.Option[bool]
|
AutoInit optional.Option[bool]
|
||||||
|
IsTemplate optional.Option[bool]
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateDeclarativeRepoWithOptions(t *testing.T, owner *user_model.User, opts DeclarativeRepoOptions) (*repo_model.Repository, string, func()) {
|
func CreateDeclarativeRepoWithOptions(t *testing.T, owner *user_model.User, opts DeclarativeRepoOptions) (*repo_model.Repository, string, func()) {
|
||||||
|
@ -731,6 +732,7 @@ func CreateDeclarativeRepoWithOptions(t *testing.T, owner *user_model.User, opts
|
||||||
License: "WTFPL",
|
License: "WTFPL",
|
||||||
Readme: "Default",
|
Readme: "Default",
|
||||||
DefaultBranch: "main",
|
DefaultBranch: "main",
|
||||||
|
IsTemplate: opts.IsTemplate.Value(),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, repo)
|
assert.NotEmpty(t, repo)
|
||||||
|
|
1099
tests/integration/quota_use_test.go
Normal file
1099
tests/integration/quota_use_test.go
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue