mirror of
https://github.com/go-gitea/gitea
synced 2024-11-09 03:34:24 +00:00
465 lines
13 KiB
Go
465 lines
13 KiB
Go
|
// 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,
|
||
|
})
|
||
|
}
|