Merge remote-tracking branch 'upstream/main' into limit-repo-size

This commit is contained in:
DmitryFrolovTri committed 2023-03-22 17:06:44 +00:00
commit 3f8bdd1c38
671 files changed
+14765 -18377

No files matched your search

+15 -2
View File
@@ -21,8 +21,11 @@ import (
)
const (
uuidHeaderKey = "x-runner-uuid"
tokenHeaderKey = "x-runner-token"
uuidHeaderKey = "x-runner-uuid"
tokenHeaderKey = "x-runner-token"
versionHeaderKey = "x-runner-version"
versionUnknown = "Unknown"
)
var withRunner = connect.WithInterceptors(connect.UnaryInterceptorFunc(func(unaryFunc connect.UnaryFunc) connect.UnaryFunc {
@@ -33,6 +36,12 @@ var withRunner = connect.WithInterceptors(connect.UnaryInterceptorFunc(func(unar
}
uuid := request.Header().Get(uuidHeaderKey)
token := request.Header().Get(tokenHeaderKey)
version := request.Header().Get(versionHeaderKey)
if util.IsEmptyString(version) {
version = versionUnknown
}
version, _ = util.SplitStringAtByteN(version, 64)
runner, err := actions_model.GetRunnerByUUID(ctx, uuid)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
@@ -45,6 +54,10 @@ var withRunner = connect.WithInterceptors(connect.UnaryInterceptorFunc(func(unar
}
cols := []string{"last_online"}
if runner.Version != version {
runner.Version = version
cols = append(cols, "version")
}
runner.LastOnline = timeutil.TimeStampNow()
if methodName == "UpdateTask" || methodName == "UpdateLog" {
runner.LastActive = timeutil.TimeStampNow()
+50
View File
@@ -0,0 +1,50 @@
# Gitea Package Registry
This document gives a brief overview how the package registry is organized in code.
## Structure
The package registry code is divided into multiple modules to split the functionality and make code reuse possible.
| Module | Description |
| - | - |
| `models/packages` | Common methods and models used by all registry types |
| `models/packages/<type>` | Methods used by specific registry type. There should be no need to use type specific models. |
| `modules/packages` | Common methods and types used by multiple registry types |
| `modules/packages/<type>` | Registry type specific methods and types (e.g. metadata extraction of package files) |
| `routers/api/packages` | Route definitions for all registry types |
| `routers/api/packages/<type>` | Route implementation for a specific registry type |
| `services/packages` | Helper methods used by registry types to handle common tasks like package creation and deletion in `routers` |
| `services/packages/<type>` | Registry type specific methods used by `routers` and `services` |
## Models
Every package registry implementation uses the same underlaying models:
| Model | Description |
| - | - |
| `Package` | The root of a package providing values fixed for every version (e.g. the package name) |
| `PackageVersion` | A version of a package containing metadata (e.g. the package description) |
| `PackageFile` | A file of a package describing its content (e.g. file name) |
| `PackageBlob` | The content of a file (may be shared by multiple files) |
| `PackageProperty` | Additional properties attached to `Package`, `PackageVersion` or `PackageFile` (e.g. used if metadata is needed for routing) |
The following diagram shows the relationship between the models:
```
Package <1---*> PackageVersion <1---*> PackageFile <*---1> PackageBlob
```
## Adding a new package registry type
Before adding a new package registry type have a look at the existing implementation to get an impression of how it could work.
Most registry types offer endpoints to retrieve the metadata, upload and download package files.
The upload endpoint is often the heavy part because it must validate the uploaded blob, extract metadata and create the models.
The methods to validate and extract the metadata should be added in the `modules/packages/<type>` package.
If the upload is valid the methods in `services/packages` allow to store the upload and create the corresponding models.
It depends if the registry type allows multiple files per package version which method should be called:
- `CreatePackageAndAddFile`: error if package version already exists
- `CreatePackageOrAddFileToExisting`: error if file already exists
- `AddFileToExistingPackage`: error if package version does not exist or file already exists
`services/packages` also contains helper methods to download a file or to remove a package version.
There are no helper methods for metadata endpoints because they are very type specific.
+36
View File
@@ -28,6 +28,7 @@ import (
"code.gitea.io/gitea/routers/api/packages/pub"
"code.gitea.io/gitea/routers/api/packages/pypi"
"code.gitea.io/gitea/routers/api/packages/rubygems"
"code.gitea.io/gitea/routers/api/packages/swift"
"code.gitea.io/gitea/routers/api/packages/vagrant"
"code.gitea.io/gitea/services/auth"
context_service "code.gitea.io/gitea/services/context"
@@ -375,6 +376,41 @@ func CommonRoutes(ctx gocontext.Context) *web.Route {
r.Delete("/yank", rubygems.DeletePackage)
}, reqPackageAccess(perm.AccessModeWrite))
}, reqPackageAccess(perm.AccessModeRead))
r.Group("/swift", func() {
r.Group("/{scope}/{name}", func() {
r.Group("", func() {
r.Get("", swift.EnumeratePackageVersions)
r.Get(".json", swift.EnumeratePackageVersions)
}, swift.CheckAcceptMediaType(swift.AcceptJSON))
r.Group("/{version}", func() {
r.Get("/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest)
r.Put("", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile)
r.Get("", func(ctx *context.Context) {
// Can't use normal routes here: https://github.com/go-chi/chi/issues/781
version := ctx.Params("version")
if strings.HasSuffix(version, ".zip") {
swift.CheckAcceptMediaType(swift.AcceptZip)(ctx)
if ctx.Written() {
return
}
ctx.SetParams("version", version[:len(version)-4])
swift.DownloadPackageFile(ctx)
} else {
swift.CheckAcceptMediaType(swift.AcceptJSON)(ctx)
if ctx.Written() {
return
}
if strings.HasSuffix(version, ".json") {
ctx.SetParams("version", version[:len(version)-5])
}
swift.PackageVersionMetadata(ctx)
}
})
})
})
r.Get("/identifiers", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.LookupPackageIdentifiers)
}, reqPackageAccess(perm.AccessModeRead))
r.Group("/vagrant", func() {
r.Group("/authenticate", func() {
r.Get("", vagrant.CheckAuthenticate)
+1
View File
@@ -45,6 +45,7 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
Author: npm_module.User{Name: metadata.Author},
License: metadata.License,
Versions: versions,
Repository: metadata.Repository,
}
}
+6
View File
@@ -8,6 +8,7 @@ import (
"io"
"net/http"
"regexp"
"sort"
"strings"
packages_model "code.gitea.io/gitea/models/packages"
@@ -62,6 +63,11 @@ func PackageMetadata(ctx *context.Context) {
return
}
// sort package descriptors by version to mimic PyPI format
sort.Slice(pds, func(i, j int) bool {
return strings.Compare(pds[i].Version.Version, pds[j].Version.Version) < 0
})
ctx.Data["RegistryURL"] = setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/pypi"
ctx.Data["PackageDescriptor"] = pds[0]
ctx.Data["PackageDescriptors"] = pds
+464
View File
@@ -0,0 +1,464 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package swift
import (
"errors"
"fmt"
"io"
"net/http"
"regexp"
"sort"
"strings"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
swift_module "code.gitea.io/gitea/modules/packages/swift"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
packages_service "code.gitea.io/gitea/services/packages"
"github.com/hashicorp/go-version"
)
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#35-api-versioning
const (
AcceptJSON = "application/vnd.swift.registry.v1+json"
AcceptSwift = "application/vnd.swift.registry.v1+swift"
AcceptZip = "application/vnd.swift.registry.v1+zip"
)
var (
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#361-package-scope
scopePattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9-]{0,38}\z`)
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#362-package-name
namePattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9-_]{0,99}\z`)
)
type headers struct {
Status int
ContentType string
Digest string
Location string
Link string
}
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#35-api-versioning
func setResponseHeaders(resp http.ResponseWriter, h *headers) {
if h.ContentType != "" {
resp.Header().Set("Content-Type", h.ContentType)
}
if h.Digest != "" {
resp.Header().Set("Digest", "sha256="+h.Digest)
}
if h.Location != "" {
resp.Header().Set("Location", h.Location)
}
if h.Link != "" {
resp.Header().Set("Link", h.Link)
}
resp.Header().Set("Content-Version", "1")
if h.Status != 0 {
resp.WriteHeader(h.Status)
}
}
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#33-error-handling
func apiError(ctx *context.Context, status int, obj interface{}) {
// https://www.rfc-editor.org/rfc/rfc7807
type Problem struct {
Status int `json:"status"`
Detail string `json:"detail"`
}
helper.LogAndProcessError(ctx, status, obj, func(message string) {
setResponseHeaders(ctx.Resp, &headers{
Status: status,
ContentType: "application/problem+json",
})
if err := json.NewEncoder(ctx.Resp).Encode(Problem{
Status: status,
Detail: message,
}); err != nil {
log.Error("JSON encode: %v", err)
}
})
}
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#35-api-versioning
func CheckAcceptMediaType(requiredAcceptHeader string) func(ctx *context.Context) {
return func(ctx *context.Context) {
accept := ctx.Req.Header.Get("Accept")
if accept != "" && accept != requiredAcceptHeader {
apiError(ctx, http.StatusBadRequest, fmt.Sprintf("Unexpected accept header. Should be '%s'.", requiredAcceptHeader))
}
}
}
func buildPackageID(scope, name string) string {
return scope + "." + name
}
type Release struct {
URL string `json:"url"`
}
type EnumeratePackageVersionsResponse struct {
Releases map[string]Release `json:"releases"`
}
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#41-list-package-releases
func EnumeratePackageVersions(ctx *context.Context) {
packageScope := ctx.Params("scope")
packageName := ctx.Params("name")
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, buildPackageID(packageScope, packageName))
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if len(pvs) == 0 {
apiError(ctx, http.StatusNotFound, nil)
return
}
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
sort.Slice(pds, func(i, j int) bool {
return pds[i].SemVer.LessThan(pds[j].SemVer)
})
baseURL := fmt.Sprintf("%sapi/packages/%s/swift/%s/%s/", setting.AppURL, ctx.Package.Owner.LowerName, packageScope, packageName)
releases := make(map[string]Release)
for _, pd := range pds {
version := pd.SemVer.String()
releases[version] = Release{
URL: baseURL + version,
}
}
setResponseHeaders(ctx.Resp, &headers{
Link: fmt.Sprintf(`<%s%s>; rel="latest-version"`, baseURL, pds[len(pds)-1].Version.Version),
})
ctx.JSON(http.StatusOK, EnumeratePackageVersionsResponse{
Releases: releases,
})
}
type Resource struct {
Name string `json:"id"`
Type string `json:"type"`
Checksum string `json:"checksum"`
}
type PackageVersionMetadataResponse struct {
ID string `json:"id"`
Version string `json:"version"`
Resources []Resource `json:"resources"`
Metadata *swift_module.SoftwareSourceCode `json:"metadata"`
}
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-2
func PackageVersionMetadata(ctx *context.Context) {
id := buildPackageID(ctx.Params("scope"), ctx.Params("name"))
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, id, ctx.Params("version"))
if err != nil {
if errors.Is(err, util.ErrNotExist) {
apiError(ctx, http.StatusNotFound, err)
} else {
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
pd, err := packages_model.GetPackageDescriptor(ctx, pv)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
metadata := pd.Metadata.(*swift_module.Metadata)
setResponseHeaders(ctx.Resp, &headers{})
ctx.JSON(http.StatusOK, PackageVersionMetadataResponse{
ID: id,
Version: pd.Version.Version,
Resources: []Resource{
{
Name: "source-archive",
Type: "application/zip",
Checksum: pd.Files[0].Blob.HashSHA256,
},
},
Metadata: &swift_module.SoftwareSourceCode{
Context: []string{"http://schema.org/"},
Type: "SoftwareSourceCode",
Name: pd.PackageProperties.GetByName(swift_module.PropertyName),
Version: pd.Version.Version,
Description: metadata.Description,
Keywords: metadata.Keywords,
CodeRepository: metadata.RepositoryURL,
License: metadata.License,
ProgrammingLanguage: swift_module.ProgrammingLanguage{
Type: "ComputerLanguage",
Name: "Swift",
URL: "https://swift.org",
},
Author: swift_module.Person{
Type: "Person",
GivenName: metadata.Author.GivenName,
MiddleName: metadata.Author.MiddleName,
FamilyName: metadata.Author.FamilyName,
},
},
})
}
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#43-fetch-manifest-for-a-package-release
func DownloadManifest(ctx *context.Context) {
packageScope := ctx.Params("scope")
packageName := ctx.Params("name")
packageVersion := ctx.Params("version")
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, buildPackageID(packageScope, packageName), packageVersion)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
apiError(ctx, http.StatusNotFound, err)
} else {
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
pd, err := packages_model.GetPackageDescriptor(ctx, pv)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
swiftVersion := ctx.FormTrim("swift-version")
if swiftVersion != "" {
v, err := version.NewVersion(swiftVersion)
if err == nil {
swiftVersion = swift_module.TrimmedVersionString(v)
}
}
m, ok := pd.Metadata.(*swift_module.Metadata).Manifests[swiftVersion]
if !ok {
setResponseHeaders(ctx.Resp, &headers{
Status: http.StatusSeeOther,
Location: fmt.Sprintf("%sapi/packages/%s/swift/%s/%s/%s/Package.swift", setting.AppURL, ctx.Package.Owner.LowerName, packageScope, packageName, packageVersion),
})
return
}
setResponseHeaders(ctx.Resp, &headers{})
filename := "Package.swift"
if swiftVersion != "" {
filename = fmt.Sprintf("Package@swift-%s.swift", swiftVersion)
}
ctx.ServeContent(strings.NewReader(m.Content), &context.ServeHeaderOptions{
ContentType: "text/x-swift",
Filename: filename,
LastModified: pv.CreatedUnix.AsLocalTime(),
})
}
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-6
func UploadPackageFile(ctx *context.Context) {
packageScope := ctx.Params("scope")
packageName := ctx.Params("name")
v, err := version.NewVersion(ctx.Params("version"))
if !scopePattern.MatchString(packageScope) || !namePattern.MatchString(packageName) || err != nil {
apiError(ctx, http.StatusBadRequest, err)
return
}
packageVersion := v.Core().String()
file, _, err := ctx.Req.FormFile("source-archive")
if err != nil {
apiError(ctx, http.StatusBadRequest, err)
return
}
defer file.Close()
buf, err := packages_module.CreateHashedBufferFromReader(file, 32*1024*1024)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer buf.Close()
var mr io.Reader
metadata := ctx.Req.FormValue("metadata")
if metadata != "" {
mr = strings.NewReader(metadata)
}
pck, err := swift_module.ParsePackage(buf, buf.Size(), mr)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
apiError(ctx, http.StatusBadRequest, err)
} else {
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
if _, err := buf.Seek(0, io.SeekStart); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
pv, _, err := packages_service.CreatePackageAndAddFile(
&packages_service.PackageCreationInfo{
PackageInfo: packages_service.PackageInfo{
Owner: ctx.Package.Owner,
PackageType: packages_model.TypeSwift,
Name: buildPackageID(packageScope, packageName),
Version: packageVersion,
},
SemverCompatible: true,
Creator: ctx.Doer,
Metadata: pck.Metadata,
PackageProperties: map[string]string{
swift_module.PropertyScope: packageScope,
swift_module.PropertyName: packageName,
},
},
&packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
Filename: fmt.Sprintf("%s-%s.zip", packageName, packageVersion),
},
Creator: ctx.Doer,
Data: buf,
IsLead: true,
},
)
if err != nil {
switch err {
case packages_model.ErrDuplicatePackageVersion:
apiError(ctx, http.StatusConflict, err)
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
apiError(ctx, http.StatusForbidden, err)
default:
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
for _, url := range pck.RepositoryURLs {
_, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, swift_module.PropertyRepositoryURL, url)
if err != nil {
log.Error("InsertProperty failed: %v", err)
}
}
setResponseHeaders(ctx.Resp, &headers{})
ctx.Status(http.StatusCreated)
}
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-4
func DownloadPackageFile(ctx *context.Context) {
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, buildPackageID(ctx.Params("scope"), ctx.Params("name")), ctx.Params("version"))
if err != nil {
if errors.Is(err, util.ErrNotExist) {
apiError(ctx, http.StatusNotFound, err)
} else {
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
pd, err := packages_model.GetPackageDescriptor(ctx, pv)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
pf := pd.Files[0].File
s, _, err := packages_service.GetPackageFileStream(ctx, pf)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer s.Close()
setResponseHeaders(ctx.Resp, &headers{
Digest: pd.Files[0].Blob.HashSHA256,
})
ctx.ServeContent(s, &context.ServeHeaderOptions{
Filename: pf.Name,
ContentType: "application/zip",
LastModified: pf.CreatedUnix.AsLocalTime(),
})
}
type LookupPackageIdentifiersResponse struct {
Identifiers []string `json:"identifiers"`
}
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-5
func LookupPackageIdentifiers(ctx *context.Context) {
url := ctx.FormTrim("url")
if url == "" {
apiError(ctx, http.StatusBadRequest, nil)
return
}
pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeSwift,
Properties: map[string]string{
swift_module.PropertyRepositoryURL: url,
},
IsInternal: util.OptionalBoolFalse,
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if len(pvs) == 0 {
apiError(ctx, http.StatusNotFound, nil)
return
}
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
identifiers := make([]string, 0, len(pds))
for _, pd := range pds {
identifiers = append(identifiers, pd.Package.Name)
}
setResponseHeaders(ctx.Resp, &headers{})
ctx.JSON(http.StatusOK, LookupPackageIdentifiersResponse{
Identifiers: identifiers,
})
}
+87
View File
@@ -0,0 +1,87 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"net/http"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/convert"
)
// GetAllEmails
func GetAllEmails(ctx *context.APIContext) {
// swagger:operation GET /admin/emails admin adminGetAllEmails
// ---
// summary: List all emails
// 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/EmailList"
// "403":
// "$ref": "#/responses/forbidden"
listOptions := utils.GetListOptions(ctx)
emails, maxResults, err := user_model.SearchEmails(&user_model.SearchEmailOptions{
Keyword: ctx.Params(":email"),
ListOptions: listOptions,
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetAllEmails", err)
return
}
results := make([]*api.Email, len(emails))
for i := range emails {
results[i] = convert.ToEmailSearch(emails[i])
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &results)
}
// SearchEmail
func SearchEmail(ctx *context.APIContext) {
// swagger:operation GET /admin/emails/search admin adminSearchEmails
// ---
// summary: Search all emails
// produces:
// - application/json
// parameters:
// - name: q
// in: query
// description: keyword
// type: string
// - 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/EmailList"
// "403":
// "$ref": "#/responses/forbidden"
ctx.SetParams(":email", ctx.FormTrim("q"))
GetAllEmails(ctx)
}
+2
View File
@@ -32,6 +32,8 @@ func CreateRepo(ctx *context.APIContext) {
// responses:
// "201":
// "$ref": "#/responses/Repository"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
+78 -5
View File
@@ -305,6 +305,10 @@ func DeleteUser(ctx *context.APIContext) {
// description: username of user to delete
// type: string
// required: true
// - name: purge
// in: query
// description: purge the user from the system completely
// type: boolean
// responses:
// "204":
// "$ref": "#/responses/empty"
@@ -413,14 +417,23 @@ func DeleteUserPublicKey(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}
// GetAllUsers API for getting information of all the users
func GetAllUsers(ctx *context.APIContext) {
// swagger:operation GET /admin/users admin adminGetAllUsers
// SearchUsers API for getting information of the users according the filter conditions
func SearchUsers(ctx *context.APIContext) {
// swagger:operation GET /admin/users admin adminSearchUsers
// ---
// summary: List all users
// summary: Search users according filter conditions
// produces:
// - application/json
// parameters:
// - name: source_id
// in: query
// description: ID of the user's login source to search for
// type: integer
// format: int64
// - name: login_name
// in: query
// description: user's login name to search for
// type: string
// - name: page
// in: query
// description: page number of results to return (1-based)
@@ -440,11 +453,13 @@ func GetAllUsers(ctx *context.APIContext) {
users, maxResults, err := user_model.SearchUsers(&user_model.SearchUserOptions{
Actor: ctx.Doer,
Type: user_model.UserTypeIndividual,
LoginName: ctx.FormTrim("login_name"),
SourceID: ctx.FormInt64("source_id"),
OrderBy: db.SearchOrderByAlphabetically,
ListOptions: listOptions,
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetAllUsers", err)
ctx.Error(http.StatusInternalServerError, "SearchUsers", err)
return
}
@@ -457,3 +472,61 @@ func GetAllUsers(ctx *context.APIContext) {
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &results)
}
// RenameUser api for renaming a user
func RenameUser(ctx *context.APIContext) {
// swagger:operation POST /admin/users/{username}/rename admin adminRenameUser
// ---
// summary: Rename a user
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: existing username of user
// type: string
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/RenameUserOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
if ctx.ContextUser.IsOrganization() {
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
return
}
newName := web.GetForm(ctx).(*api.RenameUserOption).NewName
if strings.EqualFold(newName, ctx.ContextUser.Name) {
// Noop as username is not changed
ctx.Status(http.StatusNoContent)
return
}
// Check if user name has been changed
if err := user_service.RenameUser(ctx, ctx.ContextUser, newName); err != nil {
switch {
case user_model.IsErrUserAlreadyExist(err):
ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("form.username_been_taken"))
case db.IsErrNameReserved(err):
ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_reserved", newName))
case db.IsErrNamePatternNotAllowed(err):
ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_pattern_not_allowed", newName))
case db.IsErrNameCharsNotAllowed(err):
ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_chars_not_allowed", newName))
default:
ctx.ServerError("ChangeUserName", err)
}
return
}
ctx.Status(http.StatusNoContent)
}
+6 -1
View File
@@ -1245,7 +1245,7 @@ func Routes(ctx gocontext.Context) *web.Route {
})
m.Get("/orgs", admin.GetAllOrgs)
m.Group("/users", func() {
m.Get("", admin.GetAllUsers)
m.Get("", admin.SearchUsers)
m.Post("", bind(api.CreateUserOption{}), admin.CreateUser)
m.Group("/{username}", func() {
m.Combo("").Patch(bind(api.EditUserOption{}), admin.EditUser).
@@ -1257,8 +1257,13 @@ func Routes(ctx gocontext.Context) *web.Route {
m.Get("/orgs", org.ListUserOrgs)
m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg)
m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
m.Post("/rename", bind(api.RenameUserOption{}), admin.RenameUser)
}, context_service.UserAssignmentAPI())
})
m.Group("/emails", func() {
m.Get("", admin.GetAllEmails)
m.Get("/search", admin.SearchEmail)
})
m.Group("/unadopted", func() {
m.Get("", admin.ListUnadoptedRepositories)
m.Post("/{username}/{reponame}", admin.AdoptRepository)
+1 -1
View File
@@ -40,7 +40,7 @@ func ListPackages(ctx *context.APIContext) {
// in: query
// description: package type filter
// type: string
// enum: [cargo, chef, composer, conan, conda, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems, vagrant]
// enum: [cargo, chef, composer, conan, conda, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems, swift, vagrant]
// - name: q
// in: query
// description: name filter
+1 -1
View File
@@ -154,7 +154,7 @@ func Migrate(ctx *context.APIContext) {
Issues: form.Issues,
Milestones: form.Milestones,
Labels: form.Labels,
Comments: true,
Comments: form.Issues || form.PullRequests,
PullRequests: form.PullRequests,
Releases: form.Releases,
GitServiceType: gitServiceType,
+44
View File
@@ -231,6 +231,13 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
if opt.AutoInit && opt.Readme == "" {
opt.Readme = "Default"
}
// If the readme template does not exist, a 400 will be returned.
if opt.AutoInit && len(opt.Readme) > 0 && !util.SliceContains(repo_module.Readmes, opt.Readme) {
ctx.Error(http.StatusBadRequest, "", fmt.Errorf("readme template does not exist, available templates: %v", repo_module.Readmes))
return
}
repo, err := repo_service.CreateRepository(ctx, ctx.Doer, owner, repo_module.CreateRepoOptions{
Name: opt.Name,
Description: opt.Description,
@@ -284,6 +291,8 @@ func Create(ctx *context.APIContext) {
// responses:
// "201":
// "$ref": "#/responses/Repository"
// "400":
// "$ref": "#/responses/error"
// "409":
// description: The repository with the same name already exists.
// "422":
@@ -465,6 +474,8 @@ func CreateOrgRepo(ctx *context.APIContext) {
// responses:
// "201":
// "$ref": "#/responses/Repository"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "403":
@@ -930,6 +941,39 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
}
}
if opts.HasReleases != nil && !unit_model.TypeReleases.UnitGlobalDisabled() {
if *opts.HasReleases {
units = append(units, repo_model.RepoUnit{
RepoID: repo.ID,
Type: unit_model.TypeReleases,
})
} else {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeReleases)
}
}
if opts.HasPackages != nil && !unit_model.TypePackages.UnitGlobalDisabled() {
if *opts.HasPackages {
units = append(units, repo_model.RepoUnit{
RepoID: repo.ID,
Type: unit_model.TypePackages,
})
} else {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages)
}
}
if opts.HasActions != nil && !unit_model.TypeActions.UnitGlobalDisabled() {
if *opts.HasActions {
units = append(units, repo_model.RepoUnit{
RepoID: repo.ID,
Type: unit_model.TypeActions,
})
} else {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeActions)
}
}
if err := repo_model.UpdateRepositoryUnits(repo, units, deleteUnitTypes); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateRepositoryUnits", err)
return err
+3
View File
@@ -48,6 +48,9 @@ type swaggerParameterBodies struct {
// in:body
CreateKeyOption api.CreateKeyOption
// in:body
RenameUserOption api.RenameUserOption
// in:body
CreateLabelOption api.CreateLabelOption
// in:body
+1 -1
View File
@@ -31,7 +31,7 @@ func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
return
}
if err := repos.LoadAttributes(); err != nil {
if err := repos.LoadAttributes(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "RepositoryList.LoadAttributes", err)
return
}