feat(quota): Humble beginnings of a quota engine
This is an implementation of a quota engine, and the API routes to manage its settings. This does *not* contain any enforcement code: this is just the bedrock, the engine itself. The goal of the engine is to be flexible and future proof: to be nimble enough to build on it further, without having to rewrite large parts of it. It might feel a little more complicated than necessary, because the goal was to be able to support scenarios only very few Forgejo instances need, scenarios the vast majority of mostly smaller instances simply do not care about. The goal is to support both big and small, and for that, we need a solid, flexible foundation. There are thee big parts to the engine: counting quota use, setting limits, and evaluating whether the usage is within the limits. Sounds simple on paper, less so in practice! Quota counting ============== Quota is counted based on repo ownership, whenever possible, because repo owners are in ultimate control over the resources they use: they can delete repos, attachments, everything, even if they don't *own* those themselves. They can clean up, and will always have the permission and access required to do so. Would we count quota based on the owning user, that could lead to situations where a user is unable to free up space, because they uploaded a big attachment to a repo that has been taken private since. It's both more fair, and much safer to count quota against repo owners. This means that if user A uploads an attachment to an issue opened against organization O, that will count towards the quota of organization O, rather than user A. One's quota usage stats can be queried using the `/user/quota` API endpoint. To figure out what's eating into it, the `/user/repos?order_by=size`, `/user/quota/attachments`, `/user/quota/artifacts`, and `/user/quota/packages` endpoints should be consulted. There's also `/user/quota/check?subject=<...>` to check whether the signed-in user is within a particular quota limit. Quotas are counted based on sizes stored in the database. Setting quota limits ==================== There are different "subjects" one can limit usage for. At this time, only size-based limits are implemented, which are: - `size:all`: As the name would imply, the total size of everything Forgejo tracks. - `size:repos:all`: The total size of all repositories (not including LFS). - `size:repos:public`: The total size of all public repositories (not including LFS). - `size:repos:private`: The total size of all private repositories (not including LFS). - `sizeall`: The total size of all git data (including all repositories, and LFS). - `size
lfs`: The size of all git LFS data (either in private or public repos). - `size:assets:all`: The size of all assets tracked by Forgejo. - `size:assets:attachments:all`: The size of all kinds of attachments tracked by Forgejo. - `size:assets:attachments:issues`: Size of all attachments attached to issues, including issue comments. - `size:assets:attachments:releases`: Size of all attachments attached to releases. This does *not* include automatically generated archives. - `size:assets:artifacts`: Size of all Action artifacts. - `size:assets:packages:all`: Size of all Packages. - `size:wiki`: Wiki size Wiki size is currently not tracked, and the engine will always deem it within quota. These subjects are built into Rules, which set a limit on *all* subjects within a rule. Thus, we can create a rule that says: "1Gb limit on all release assets, all packages, and git LFS, combined". For a rule to stand, the total sum of all subjects must be below the rule's limit. Rules are in turn collected into groups. A group is just a name, and a list of rules. For a group to stand, all of its rules must stand. Thus, if we have a group with two rules, one that sets a combined 1Gb limit on release assets, all packages, and git LFS, and another rule that sets a 256Mb limit on packages, if the user has 512Mb of packages, the group will not stand, because the second rule deems it over quota. Similarly, if the user has only 128Mb of packages, but 900Mb of release assets, the group will not stand, because the combined size of packages and release assets is over the 1Gb limit of the first rule. Groups themselves are collected into Group Lists. A group list stands when *any* of the groups within stand. This allows an administrator to set conservative defaults, but then place select users into additional groups that increase some aspect of their limits. To top it off, it is possible to set the default quota groups a user belongs to in `app.ini`. If there's no explicit assignment, the engine will use the default groups. This makes it possible to avoid having to assign each and every user a list of quota groups, and only those need to be explicitly assigned who need a different set of groups than the defaults. If a user has any quota groups assigned to them, the default list will not be considered for them. The management APIs =================== This commit contains the engine itself, its unit tests, and the quota management APIs. It does not contain any enforcement. The APIs are documented in-code, and in the swagger docs, and the integration tests can serve as an example on how to use them. Signed-off-by: Gergely Nagy <forgejo@gergo.csillger.hu>
This commit is contained in:
parent
250f87db59
commit
e1fe3bbdc0
28 changed files with 5435 additions and 6 deletions
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 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
|
||||
|
||||
// Package v1 Gitea API
|
||||
|
@ -892,6 +892,15 @@ func Routes() *web.Route {
|
|||
// Users (requires user scope)
|
||||
m.Group("/user", func() {
|
||||
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.Get("", user.GetUserSettings)
|
||||
m.Patch("", bind(api.UserSettingsOptions{}), user.UpdateUserSettings)
|
||||
|
@ -1482,6 +1491,16 @@ func Routes() *web.Route {
|
|||
}, reqToken(), reqOrgOwnership())
|
||||
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.Get("/list_blocked", org.ListBlockedUsers)
|
||||
m.Group("", func() {
|
||||
|
@ -1531,6 +1550,12 @@ func Routes() *web.Route {
|
|||
m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg)
|
||||
m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
|
||||
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())
|
||||
})
|
||||
m.Group("/emails", func() {
|
||||
|
@ -1552,6 +1577,37 @@ func Routes() *web.Route {
|
|||
m.Group("/runners", func() {
|
||||
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())
|
||||
|
||||
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)
|
||||
}
|
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
|
||||
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
|
||||
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)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue