mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 11:28:24 +00:00 
			
		
		
		
	Fixes #35159 Swift Package Manager expects an 'author.name' field in package metadata, but Gitea was only providing schema.org format fields (givenName, middleName, familyName). This caused SPM to fail with keyNotFound error when fetching package metadata. Changes: - Add 'name' field to Person struct (inherited from https://schema.org/Thing) - Populate 'name' field in API response using existing String() method - Maintains backward compatibility with existing schema.org fields - Provides both formats for maximum compatibility The fix ensures Swift Package Manager can successfully resolve packages while preserving full schema.org compliance.
		
			
				
	
	
		
			494 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			494 lines
		
	
	
		
			14 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/json"
 | |
| 	"code.gitea.io/gitea/modules/log"
 | |
| 	"code.gitea.io/gitea/modules/optional"
 | |
| 	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"
 | |
| 	"code.gitea.io/gitea/services/context"
 | |
| 	packages_service "code.gitea.io/gitea/services/packages"
 | |
| 
 | |
| 	"github.com/hashicorp/go-version"
 | |
| )
 | |
| 
 | |
| // https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/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/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#361-package-scope
 | |
| 	scopePattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9-]{0,38}\z`)
 | |
| 	// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/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/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/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/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#33-error-handling
 | |
| func apiError(ctx *context.Context, status int, obj any) {
 | |
| 	// https://www.rfc-editor.org/rfc/rfc7807
 | |
| 	type Problem struct {
 | |
| 		Status int    `json:"status"`
 | |
| 		Detail string `json:"detail"`
 | |
| 	}
 | |
| 
 | |
| 	message := helper.ProcessErrorForUser(ctx, status, obj)
 | |
| 	setResponseHeaders(ctx.Resp, &headers{
 | |
| 		Status:      status,
 | |
| 		ContentType: "application/problem+json",
 | |
| 	})
 | |
| 	_ = json.NewEncoder(ctx.Resp).Encode(Problem{
 | |
| 		Status: status,
 | |
| 		Detail: message,
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/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))
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/PackageRegistryUsage.md#registry-authentication
 | |
| func CheckAuthenticate(ctx *context.Context) {
 | |
| 	if ctx.Doer == nil {
 | |
| 		apiError(ctx, http.StatusUnauthorized, nil)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	ctx.Status(http.StatusOK)
 | |
| }
 | |
| 
 | |
| 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/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#41-list-package-releases
 | |
| func EnumeratePackageVersions(ctx *context.Context) {
 | |
| 	packageScope := ctx.PathParam("scope")
 | |
| 	packageName := ctx.PathParam("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:"name"`
 | |
| 	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/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-2
 | |
| func PackageVersionMetadata(ctx *context.Context) {
 | |
| 	id := buildPackageID(ctx.PathParam("scope"), ctx.PathParam("name"))
 | |
| 
 | |
| 	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, id, ctx.PathParam("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",
 | |
| 				Name:       metadata.Author.String(),
 | |
| 				GivenName:  metadata.Author.GivenName,
 | |
| 				MiddleName: metadata.Author.MiddleName,
 | |
| 				FamilyName: metadata.Author.FamilyName,
 | |
| 			},
 | |
| 		},
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#43-fetch-manifest-for-a-package-release
 | |
| func DownloadManifest(ctx *context.Context) {
 | |
| 	packageScope := ctx.PathParam("scope")
 | |
| 	packageName := ctx.PathParam("name")
 | |
| 	packageVersion := ctx.PathParam("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(),
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // formFileOptionalReadCloser returns (nil, nil) if the formKey is not present.
 | |
| func formFileOptionalReadCloser(ctx *context.Context, formKey string) (io.ReadCloser, error) {
 | |
| 	multipartFile, _, err := ctx.Req.FormFile(formKey)
 | |
| 	if err != nil && !errors.Is(err, http.ErrMissingFile) {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	if multipartFile != nil {
 | |
| 		return multipartFile, nil
 | |
| 	}
 | |
| 
 | |
| 	content := ctx.Req.FormValue(formKey)
 | |
| 	if content == "" {
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 	return io.NopCloser(strings.NewReader(content)), nil
 | |
| }
 | |
| 
 | |
| // UploadPackageFile refers to https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-6
 | |
| func UploadPackageFile(ctx *context.Context) {
 | |
| 	packageScope := ctx.PathParam("scope")
 | |
| 	packageName := ctx.PathParam("name")
 | |
| 
 | |
| 	v, err := version.NewVersion(ctx.PathParam("version"))
 | |
| 
 | |
| 	if !scopePattern.MatchString(packageScope) || !namePattern.MatchString(packageName) || err != nil {
 | |
| 		apiError(ctx, http.StatusBadRequest, err)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	packageVersion := v.Core().String()
 | |
| 
 | |
| 	file, err := formFileOptionalReadCloser(ctx, "source-archive")
 | |
| 	if file == nil || err != nil {
 | |
| 		apiError(ctx, http.StatusBadRequest, "unable to read source-archive file")
 | |
| 		return
 | |
| 	}
 | |
| 	defer file.Close()
 | |
| 
 | |
| 	buf, err := packages_module.CreateHashedBufferFromReader(file)
 | |
| 	if err != nil {
 | |
| 		apiError(ctx, http.StatusInternalServerError, err)
 | |
| 		return
 | |
| 	}
 | |
| 	defer buf.Close()
 | |
| 
 | |
| 	mr, err := formFileOptionalReadCloser(ctx, "metadata")
 | |
| 	if err != nil {
 | |
| 		apiError(ctx, http.StatusBadRequest, "unable to read metadata file")
 | |
| 		return
 | |
| 	}
 | |
| 	if mr != nil {
 | |
| 		defer mr.Close()
 | |
| 	}
 | |
| 
 | |
| 	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(
 | |
| 		ctx,
 | |
| 		&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/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-4
 | |
| func DownloadPackageFile(ctx *context.Context) {
 | |
| 	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, buildPackageID(ctx.PathParam("scope"), ctx.PathParam("name")), ctx.PathParam("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, u, _, err := packages_service.OpenFileForDownload(ctx, pf, ctx.Req.Method)
 | |
| 	if err != nil {
 | |
| 		apiError(ctx, http.StatusInternalServerError, err)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	setResponseHeaders(ctx.Resp, &headers{
 | |
| 		Digest: pd.Files[0].Blob.HashSHA256,
 | |
| 	})
 | |
| 
 | |
| 	helper.ServePackageFile(ctx, s, u, pf, &context.ServeHeaderOptions{
 | |
| 		Filename:     pf.Name,
 | |
| 		ContentType:  "application/zip",
 | |
| 		LastModified: pf.CreatedUnix.AsLocalTime(),
 | |
| 	})
 | |
| }
 | |
| 
 | |
| type LookupPackageIdentifiersResponse struct {
 | |
| 	Identifiers []string `json:"identifiers"`
 | |
| }
 | |
| 
 | |
| // https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/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: optional.Some(false),
 | |
| 	})
 | |
| 	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,
 | |
| 	})
 | |
| }
 |