mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 19:06:18 +01:00 
			
		
		
		
	Add Swift package registry (#22404)
This PR adds a [Swift](https://www.swift.org/) package registry. 
This commit is contained in:
		
							
								
								
									
										464
									
								
								routers/api/packages/swift/swift.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										464
									
								
								routers/api/packages/swift/swift.go
									
									
									
									
									
										Normal 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, | ||||
| 	}) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user