mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-02 13:00:10 +02:00
Merge remote-tracking branch 'upstream/main' into limit-repo-size
This commit is contained in:
commit
3f8bdd1c38
671 files changed
+14765
-18377
No files matched your search
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -32,6 +32,8 @@ func CreateRepo(ctx *context.APIContext) {
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Repository"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -48,6 +48,9 @@ type swaggerParameterBodies struct {
|
||||
// in:body
|
||||
CreateKeyOption api.CreateKeyOption
|
||||
|
||||
// in:body
|
||||
RenameUserOption api.RenameUserOption
|
||||
|
||||
// in:body
|
||||
CreateLabelOption api.CreateLabelOption
|
||||
// in:body
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user