Files
gitea/routers/api/v1/repo/project.go
2026-05-03 17:42:34 +05:30

1003 lines
25 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"net/http"
"slices"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
project_model "code.gitea.io/gitea/models/project"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
project_service "code.gitea.io/gitea/services/projects"
)
func getRepoProjectByID(ctx *context.APIContext) *project_model.Project {
project, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil {
if project_model.IsErrProjectNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return nil
}
project.Repo = ctx.Repo.Repository
return project
}
func getRepoProjectColumn(ctx *context.APIContext) (*project_model.Project, *project_model.Column) {
column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("column_id"))
if err != nil {
if project_model.IsErrProjectColumnNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return nil, nil
}
project, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, column.ProjectID)
if err != nil {
if project_model.IsErrProjectNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return nil, nil
}
if project.ID != ctx.PathParamInt64("id") {
ctx.APIErrorNotFound()
return nil, nil
}
project.Repo = ctx.Repo.Repository
return project, column
}
func rejectIfClosed(ctx *context.APIContext, project *project_model.Project) bool {
if project.IsClosed {
ctx.APIError(http.StatusForbidden, "project is closed")
return true
}
return false
}
func validateColumnColor(ctx *context.APIContext, color string) bool {
if color == "" {
return true
}
if !project_model.ColumnColorPattern.MatchString(color) {
ctx.APIError(http.StatusUnprocessableEntity, "color must be a 6-digit hex string like #FF0000")
return false
}
return true
}
// ListProjects lists all projects in a repository
func ListProjects(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/projects repository repoListProjects
// ---
// summary: List projects in a repository
// description: Gitea projects only contain issues — note cards and pull requests are not modeled as project items.
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: state
// in: query
// description: State of the project (open, closed, all)
// type: string
// enum: [open, closed, all]
// default: open
// - name: page
// in: query
// description: page number of results
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/ProjectList"
// "404":
// "$ref": "#/responses/notFound"
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormTrim("state"))
listOptions := utils.GetListOptions(ctx)
projects, count, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{
ListOptions: listOptions,
RepoID: ctx.Repo.Repository.ID,
IsClosed: isClosed,
Type: project_model.TypeRepository,
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
if err := project_service.LoadIssueNumbersForProjects(ctx, projects, ctx.Doer); err != nil {
ctx.APIErrorInternal(err)
return
}
for _, p := range projects {
p.Repo = ctx.Repo.Repository
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToProjectList(ctx, projects, ctx.Doer))
}
// GetProject gets a single project
func GetProject(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/projects/{id} repository repoGetProject
// ---
// summary: Get a single project
// description: Gitea projects only contain issues — note cards and pull requests are not modeled as project items.
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/Project"
// "404":
// "$ref": "#/responses/notFound"
project := getRepoProjectByID(ctx)
if ctx.Written() {
return
}
if err := project_service.LoadIssueNumbersForProject(ctx, project, ctx.Doer); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToProject(ctx, project, ctx.Doer))
}
// CreateProject creates a new project
func CreateProject(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/projects repository repoCreateProject
// ---
// summary: Create a new project
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateProjectOption"
// responses:
// "201":
// "$ref": "#/responses/Project"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateProjectOption)
templateType, err := convert.ProjectTemplateTypeFromString(form.TemplateType)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
cardType, err := convert.ProjectCardTypeFromString(form.CardType)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
p := &project_model.Project{
RepoID: ctx.Repo.Repository.ID,
Title: form.Title,
Description: form.Description,
CreatorID: ctx.Doer.ID,
TemplateType: templateType,
CardType: cardType,
Type: project_model.TypeRepository,
}
if err := project_model.NewProject(ctx, p); err != nil {
ctx.APIErrorInternal(err)
return
}
p.Repo = ctx.Repo.Repository
ctx.JSON(http.StatusCreated, convert.ToProject(ctx, p, ctx.Doer))
}
// EditProject updates a project
func EditProject(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/projects/{id} repository repoEditProject
// ---
// summary: Edit a project
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditProjectOption"
// responses:
// "200":
// "$ref": "#/responses/Project"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
project := getRepoProjectByID(ctx)
if ctx.Written() {
return
}
form := web.GetForm(ctx).(*api.EditProjectOption)
opts := project_service.UpdateProjectOptions{
Title: optional.FromPtr(form.Title),
Description: optional.FromPtr(form.Description),
}
if form.CardType != nil {
cardType, err := convert.ProjectCardTypeFromString(*form.CardType)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
opts.CardType = optional.Some(cardType)
}
if form.State != nil {
switch *form.State {
case api.StateOpen:
opts.IsClosed = optional.Some(false)
case api.StateClosed:
opts.IsClosed = optional.Some(true)
default:
ctx.APIError(http.StatusUnprocessableEntity, "state must be 'open' or 'closed'")
return
}
}
if err := project_service.UpdateProject(ctx, project, opts); err != nil {
ctx.APIErrorInternal(err)
return
}
if err := project_service.LoadIssueNumbersForProject(ctx, project, ctx.Doer); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToProject(ctx, project, ctx.Doer))
}
// DeleteProject deletes a project
func DeleteProject(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/projects/{id} repository repoDeleteProject
// ---
// summary: Delete a project
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
project := getRepoProjectByID(ctx)
if ctx.Written() {
return
}
if err := project_model.DeleteProjectByID(ctx, project.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// ListProjectColumns lists all columns in a project
func ListProjectColumns(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/projects/{id}/columns repository repoListProjectColumns
// ---
// summary: List columns in a project
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// 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/ProjectColumnList"
// "404":
// "$ref": "#/responses/notFound"
project := getRepoProjectByID(ctx)
if ctx.Written() {
return
}
total, err := project_model.CountProjectColumns(ctx, project.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
listOptions := utils.GetListOptions(ctx)
columns, err := project_model.GetProjectColumns(ctx, project.ID, listOptions)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, convert.ToProjectColumnList(ctx, columns, ctx.Doer))
}
// CreateProjectColumn creates a new column in a project
func CreateProjectColumn(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/projects/{id}/columns repository repoCreateProjectColumn
// ---
// summary: Create a new column in a project
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateProjectColumnOption"
// responses:
// "201":
// "$ref": "#/responses/ProjectColumn"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
project := getRepoProjectByID(ctx)
if ctx.Written() {
return
}
if rejectIfClosed(ctx, project) {
return
}
form := web.GetForm(ctx).(*api.CreateProjectColumnOption)
if !validateColumnColor(ctx, form.Color) {
return
}
column := &project_model.Column{
Title: form.Title,
Color: form.Color,
ProjectID: project.ID,
CreatorID: ctx.Doer.ID,
}
if err := project_model.NewColumn(ctx, column); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, convert.ToProjectColumn(ctx, column, ctx.Doer))
}
// EditProjectColumn updates a column
func EditProjectColumn(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/projects/{id}/columns/{column_id} repository repoEditProjectColumn
// ---
// summary: Edit a project column
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditProjectColumnOption"
// responses:
// "200":
// "$ref": "#/responses/ProjectColumn"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
project, column := getRepoProjectColumn(ctx)
if ctx.Written() {
return
}
if rejectIfClosed(ctx, project) {
return
}
form := web.GetForm(ctx).(*api.EditProjectColumnOption)
if form.Color != nil && !validateColumnColor(ctx, *form.Color) {
return
}
if form.Title != nil {
column.Title = *form.Title
}
if form.Color != nil {
column.Color = *form.Color
}
if form.Sorting != nil {
if *form.Sorting < -128 || *form.Sorting > 127 {
ctx.APIError(http.StatusBadRequest, "sorting value out of range, must be between -128 and 127")
return
}
column.Sorting = int8(*form.Sorting)
}
if err := project_model.UpdateColumn(ctx, column); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToProjectColumn(ctx, column, ctx.Doer))
}
// DeleteProjectColumn deletes a column
func DeleteProjectColumn(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/projects/{id}/columns/{column_id} repository repoDeleteProjectColumn
// ---
// summary: Delete a project column
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
project, column := getRepoProjectColumn(ctx)
if ctx.Written() {
return
}
if rejectIfClosed(ctx, project) {
return
}
if err := project_model.DeleteColumnByID(ctx, column.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// ListProjectColumnIssues lists all issues in a project column
func ListProjectColumnIssues(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/projects/{id}/columns/{column_id}/issues repository repoListProjectColumnIssues
// ---
// summary: List issues in a project column
// description: Gitea projects only contain issues — note cards and pull requests are not modeled as project items.
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// 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/IssueList"
// "404":
// "$ref": "#/responses/notFound"
_, column := getRepoProjectColumn(ctx)
if ctx.Written() {
return
}
listOptions := utils.GetListOptions(ctx)
issuesOpts := &issues_model.IssuesOptions{
Paginator: &listOptions,
RepoIDs: []int64{ctx.Repo.Repository.ID},
ProjectIDs: []int64{column.ProjectID},
SortType: issues_model.SortTypeProjectColumnSorting,
}
count, err := issues_model.CountIssues(ctx, issuesOpts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
issues, err := issues_model.Issues(ctx, issuesOpts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
}
// AddIssueToProjectColumn adds an issue to a project column
func AddIssueToProjectColumn(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/projects/{id}/columns/{column_id}/issues/{issue_id} repository repoAddIssueToProjectColumn
// ---
// summary: Add an issue to a project column
// description: Gitea projects only contain issues — note cards and pull requests cannot be added.
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// - name: issue_id
// in: path
// description: id of the issue
// type: integer
// format: int64
// required: true
// responses:
// "201":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
assignIssueToProjectColumn(ctx, true)
}
// RemoveIssueFromProjectColumn remove an issue from a project column
func RemoveIssueFromProjectColumn(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/projects/{id}/columns/{column_id}/issues/{issue_id} repository repoRemoveIssueFromProjectColumn
// ---
// summary: Remove an issue from a project column
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: column_id
// in: path
// description: id of the column
// type: integer
// format: int64
// required: true
// - name: issue_id
// in: path
// description: id of the issue
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
assignIssueToProjectColumn(ctx, false)
}
// assignIssueToProjectColumn assigns an issue to a project column when add is true,
// or removes the issue from any project assignment when add is false.
func assignIssueToProjectColumn(ctx *context.APIContext, add bool) {
project, column := getRepoProjectColumn(ctx)
if ctx.Written() {
return
}
if rejectIfClosed(ctx, project) {
return
}
issue, err := issues_model.GetIssueByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("issue_id"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
if err := issue.LoadProjects(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
currentProjectIDs := make([]int64, 0, len(issue.Projects))
for _, p := range issue.Projects {
currentProjectIDs = append(currentProjectIDs, p.ID)
}
if !add {
exists, err := project_model.IsIssueInColumn(ctx, issue.ID, column.ProjectID, column.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !exists {
ctx.APIErrorNotFound()
return
}
newProjectIDs := make([]int64, 0, len(currentProjectIDs))
for _, id := range currentProjectIDs {
if id != column.ProjectID {
newProjectIDs = append(newProjectIDs, id)
}
}
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, newProjectIDs); err != nil {
ctx.APIErrorInternal(err)
return
}
} else {
// Check if issue is already in this project
alreadyInProject := slices.Contains(currentProjectIDs, column.ProjectID)
if !alreadyInProject {
// Add to project first (lands in default column)
newProjectIDs := make([]int64, len(currentProjectIDs)+1)
copy(newProjectIDs, currentProjectIDs)
newProjectIDs[len(currentProjectIDs)] = column.ProjectID
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, newProjectIDs); err != nil {
ctx.APIErrorInternal(err)
return
}
}
// Move to target column
if err := project_model.MoveIssueToColumn(ctx, issue.ID, column.ProjectID, column.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
}
if add {
ctx.Status(http.StatusCreated)
} else {
ctx.Status(http.StatusNoContent)
}
}
// MoveProjectIssue moves an issue between columns of the same project (and optionally sets sorting).
func MoveProjectIssue(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/projects/{id}/issues/{issue_id}/move repository repoMoveProjectIssue
// ---
// summary: Move an issue between columns of a project
// description: Atomically moves an existing project issue into a different column, optionally setting its sorting position.
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the project
// type: integer
// format: int64
// required: true
// - name: issue_id
// in: path
// description: id of the issue
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/MoveProjectIssueOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
project := getRepoProjectByID(ctx)
if ctx.Written() {
return
}
if rejectIfClosed(ctx, project) {
return
}
form := web.GetForm(ctx).(*api.MoveProjectIssueOption)
column, err := project_model.GetColumn(ctx, form.ColumnID)
if err != nil {
if project_model.IsErrProjectColumnNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, "target column does not exist")
} else {
ctx.APIErrorInternal(err)
}
return
}
if column.ProjectID != project.ID {
ctx.APIError(http.StatusUnprocessableEntity, "target column does not belong to this project")
return
}
issue, err := issues_model.GetIssueByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("issue_id"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
var sorting int64
if form.Sorting != nil {
sorting = *form.Sorting
} else {
next, err := project_model.GetColumnIssueNextSorting(ctx, project.ID, column.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
sorting = next
}
if err := project_service.MoveIssuesOnProjectColumn(ctx, ctx.Doer, column, map[int64]int64{sorting: issue.ID}); err != nil {
if errors.Is(err, project_service.ErrIssueNotInProject) {
ctx.APIErrorNotFound()
return
}
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}