code.gitea.io/gitea@v1.22.3/routers/api/packages/swift/swift.go (about)

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package swift
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"regexp"
    12  	"sort"
    13  	"strings"
    14  
    15  	packages_model "code.gitea.io/gitea/models/packages"
    16  	"code.gitea.io/gitea/modules/json"
    17  	"code.gitea.io/gitea/modules/log"
    18  	"code.gitea.io/gitea/modules/optional"
    19  	packages_module "code.gitea.io/gitea/modules/packages"
    20  	swift_module "code.gitea.io/gitea/modules/packages/swift"
    21  	"code.gitea.io/gitea/modules/setting"
    22  	"code.gitea.io/gitea/modules/util"
    23  	"code.gitea.io/gitea/routers/api/packages/helper"
    24  	"code.gitea.io/gitea/services/context"
    25  	packages_service "code.gitea.io/gitea/services/packages"
    26  
    27  	"github.com/hashicorp/go-version"
    28  )
    29  
    30  // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#35-api-versioning
    31  const (
    32  	AcceptJSON  = "application/vnd.swift.registry.v1+json"
    33  	AcceptSwift = "application/vnd.swift.registry.v1+swift"
    34  	AcceptZip   = "application/vnd.swift.registry.v1+zip"
    35  )
    36  
    37  var (
    38  	// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#361-package-scope
    39  	scopePattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9-]{0,38}\z`)
    40  	// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#362-package-name
    41  	namePattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9-_]{0,99}\z`)
    42  )
    43  
    44  type headers struct {
    45  	Status      int
    46  	ContentType string
    47  	Digest      string
    48  	Location    string
    49  	Link        string
    50  }
    51  
    52  // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#35-api-versioning
    53  func setResponseHeaders(resp http.ResponseWriter, h *headers) {
    54  	if h.ContentType != "" {
    55  		resp.Header().Set("Content-Type", h.ContentType)
    56  	}
    57  	if h.Digest != "" {
    58  		resp.Header().Set("Digest", "sha256="+h.Digest)
    59  	}
    60  	if h.Location != "" {
    61  		resp.Header().Set("Location", h.Location)
    62  	}
    63  	if h.Link != "" {
    64  		resp.Header().Set("Link", h.Link)
    65  	}
    66  	resp.Header().Set("Content-Version", "1")
    67  	if h.Status != 0 {
    68  		resp.WriteHeader(h.Status)
    69  	}
    70  }
    71  
    72  // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#33-error-handling
    73  func apiError(ctx *context.Context, status int, obj any) {
    74  	// https://www.rfc-editor.org/rfc/rfc7807
    75  	type Problem struct {
    76  		Status int    `json:"status"`
    77  		Detail string `json:"detail"`
    78  	}
    79  
    80  	helper.LogAndProcessError(ctx, status, obj, func(message string) {
    81  		setResponseHeaders(ctx.Resp, &headers{
    82  			Status:      status,
    83  			ContentType: "application/problem+json",
    84  		})
    85  		if err := json.NewEncoder(ctx.Resp).Encode(Problem{
    86  			Status: status,
    87  			Detail: message,
    88  		}); err != nil {
    89  			log.Error("JSON encode: %v", err)
    90  		}
    91  	})
    92  }
    93  
    94  // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#35-api-versioning
    95  func CheckAcceptMediaType(requiredAcceptHeader string) func(ctx *context.Context) {
    96  	return func(ctx *context.Context) {
    97  		accept := ctx.Req.Header.Get("Accept")
    98  		if accept != "" && accept != requiredAcceptHeader {
    99  			apiError(ctx, http.StatusBadRequest, fmt.Sprintf("Unexpected accept header. Should be '%s'.", requiredAcceptHeader))
   100  		}
   101  	}
   102  }
   103  
   104  func buildPackageID(scope, name string) string {
   105  	return scope + "." + name
   106  }
   107  
   108  type Release struct {
   109  	URL string `json:"url"`
   110  }
   111  
   112  type EnumeratePackageVersionsResponse struct {
   113  	Releases map[string]Release `json:"releases"`
   114  }
   115  
   116  // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#41-list-package-releases
   117  func EnumeratePackageVersions(ctx *context.Context) {
   118  	packageScope := ctx.Params("scope")
   119  	packageName := ctx.Params("name")
   120  
   121  	pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, buildPackageID(packageScope, packageName))
   122  	if err != nil {
   123  		apiError(ctx, http.StatusInternalServerError, err)
   124  		return
   125  	}
   126  	if len(pvs) == 0 {
   127  		apiError(ctx, http.StatusNotFound, nil)
   128  		return
   129  	}
   130  
   131  	pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
   132  	if err != nil {
   133  		apiError(ctx, http.StatusInternalServerError, err)
   134  		return
   135  	}
   136  
   137  	sort.Slice(pds, func(i, j int) bool {
   138  		return pds[i].SemVer.LessThan(pds[j].SemVer)
   139  	})
   140  
   141  	baseURL := fmt.Sprintf("%sapi/packages/%s/swift/%s/%s/", setting.AppURL, ctx.Package.Owner.LowerName, packageScope, packageName)
   142  
   143  	releases := make(map[string]Release)
   144  	for _, pd := range pds {
   145  		version := pd.SemVer.String()
   146  		releases[version] = Release{
   147  			URL: baseURL + version,
   148  		}
   149  	}
   150  
   151  	setResponseHeaders(ctx.Resp, &headers{
   152  		Link: fmt.Sprintf(`<%s%s>; rel="latest-version"`, baseURL, pds[len(pds)-1].Version.Version),
   153  	})
   154  
   155  	ctx.JSON(http.StatusOK, EnumeratePackageVersionsResponse{
   156  		Releases: releases,
   157  	})
   158  }
   159  
   160  type Resource struct {
   161  	Name     string `json:"name"`
   162  	Type     string `json:"type"`
   163  	Checksum string `json:"checksum"`
   164  }
   165  
   166  type PackageVersionMetadataResponse struct {
   167  	ID        string                           `json:"id"`
   168  	Version   string                           `json:"version"`
   169  	Resources []Resource                       `json:"resources"`
   170  	Metadata  *swift_module.SoftwareSourceCode `json:"metadata"`
   171  }
   172  
   173  // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-2
   174  func PackageVersionMetadata(ctx *context.Context) {
   175  	id := buildPackageID(ctx.Params("scope"), ctx.Params("name"))
   176  
   177  	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, id, ctx.Params("version"))
   178  	if err != nil {
   179  		if errors.Is(err, util.ErrNotExist) {
   180  			apiError(ctx, http.StatusNotFound, err)
   181  		} else {
   182  			apiError(ctx, http.StatusInternalServerError, err)
   183  		}
   184  		return
   185  	}
   186  
   187  	pd, err := packages_model.GetPackageDescriptor(ctx, pv)
   188  	if err != nil {
   189  		apiError(ctx, http.StatusInternalServerError, err)
   190  		return
   191  	}
   192  
   193  	metadata := pd.Metadata.(*swift_module.Metadata)
   194  
   195  	setResponseHeaders(ctx.Resp, &headers{})
   196  
   197  	ctx.JSON(http.StatusOK, PackageVersionMetadataResponse{
   198  		ID:      id,
   199  		Version: pd.Version.Version,
   200  		Resources: []Resource{
   201  			{
   202  				Name:     "source-archive",
   203  				Type:     "application/zip",
   204  				Checksum: pd.Files[0].Blob.HashSHA256,
   205  			},
   206  		},
   207  		Metadata: &swift_module.SoftwareSourceCode{
   208  			Context:        []string{"http://schema.org/"},
   209  			Type:           "SoftwareSourceCode",
   210  			Name:           pd.PackageProperties.GetByName(swift_module.PropertyName),
   211  			Version:        pd.Version.Version,
   212  			Description:    metadata.Description,
   213  			Keywords:       metadata.Keywords,
   214  			CodeRepository: metadata.RepositoryURL,
   215  			License:        metadata.License,
   216  			ProgrammingLanguage: swift_module.ProgrammingLanguage{
   217  				Type: "ComputerLanguage",
   218  				Name: "Swift",
   219  				URL:  "https://swift.org",
   220  			},
   221  			Author: swift_module.Person{
   222  				Type:       "Person",
   223  				GivenName:  metadata.Author.GivenName,
   224  				MiddleName: metadata.Author.MiddleName,
   225  				FamilyName: metadata.Author.FamilyName,
   226  			},
   227  		},
   228  	})
   229  }
   230  
   231  // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#43-fetch-manifest-for-a-package-release
   232  func DownloadManifest(ctx *context.Context) {
   233  	packageScope := ctx.Params("scope")
   234  	packageName := ctx.Params("name")
   235  	packageVersion := ctx.Params("version")
   236  
   237  	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, buildPackageID(packageScope, packageName), packageVersion)
   238  	if err != nil {
   239  		if errors.Is(err, util.ErrNotExist) {
   240  			apiError(ctx, http.StatusNotFound, err)
   241  		} else {
   242  			apiError(ctx, http.StatusInternalServerError, err)
   243  		}
   244  		return
   245  	}
   246  
   247  	pd, err := packages_model.GetPackageDescriptor(ctx, pv)
   248  	if err != nil {
   249  		apiError(ctx, http.StatusInternalServerError, err)
   250  		return
   251  	}
   252  
   253  	swiftVersion := ctx.FormTrim("swift-version")
   254  	if swiftVersion != "" {
   255  		v, err := version.NewVersion(swiftVersion)
   256  		if err == nil {
   257  			swiftVersion = swift_module.TrimmedVersionString(v)
   258  		}
   259  	}
   260  	m, ok := pd.Metadata.(*swift_module.Metadata).Manifests[swiftVersion]
   261  	if !ok {
   262  		setResponseHeaders(ctx.Resp, &headers{
   263  			Status:   http.StatusSeeOther,
   264  			Location: fmt.Sprintf("%sapi/packages/%s/swift/%s/%s/%s/Package.swift", setting.AppURL, ctx.Package.Owner.LowerName, packageScope, packageName, packageVersion),
   265  		})
   266  		return
   267  	}
   268  
   269  	setResponseHeaders(ctx.Resp, &headers{})
   270  
   271  	filename := "Package.swift"
   272  	if swiftVersion != "" {
   273  		filename = fmt.Sprintf("Package@swift-%s.swift", swiftVersion)
   274  	}
   275  
   276  	ctx.ServeContent(strings.NewReader(m.Content), &context.ServeHeaderOptions{
   277  		ContentType:  "text/x-swift",
   278  		Filename:     filename,
   279  		LastModified: pv.CreatedUnix.AsLocalTime(),
   280  	})
   281  }
   282  
   283  // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-6
   284  func UploadPackageFile(ctx *context.Context) {
   285  	packageScope := ctx.Params("scope")
   286  	packageName := ctx.Params("name")
   287  
   288  	v, err := version.NewVersion(ctx.Params("version"))
   289  
   290  	if !scopePattern.MatchString(packageScope) || !namePattern.MatchString(packageName) || err != nil {
   291  		apiError(ctx, http.StatusBadRequest, err)
   292  		return
   293  	}
   294  
   295  	packageVersion := v.Core().String()
   296  
   297  	file, _, err := ctx.Req.FormFile("source-archive")
   298  	if err != nil {
   299  		apiError(ctx, http.StatusBadRequest, err)
   300  		return
   301  	}
   302  	defer file.Close()
   303  
   304  	buf, err := packages_module.CreateHashedBufferFromReader(file)
   305  	if err != nil {
   306  		apiError(ctx, http.StatusInternalServerError, err)
   307  		return
   308  	}
   309  	defer buf.Close()
   310  
   311  	var mr io.Reader
   312  	metadata := ctx.Req.FormValue("metadata")
   313  	if metadata != "" {
   314  		mr = strings.NewReader(metadata)
   315  	}
   316  
   317  	pck, err := swift_module.ParsePackage(buf, buf.Size(), mr)
   318  	if err != nil {
   319  		if errors.Is(err, util.ErrInvalidArgument) {
   320  			apiError(ctx, http.StatusBadRequest, err)
   321  		} else {
   322  			apiError(ctx, http.StatusInternalServerError, err)
   323  		}
   324  		return
   325  	}
   326  
   327  	if _, err := buf.Seek(0, io.SeekStart); err != nil {
   328  		apiError(ctx, http.StatusInternalServerError, err)
   329  		return
   330  	}
   331  
   332  	pv, _, err := packages_service.CreatePackageAndAddFile(
   333  		ctx,
   334  		&packages_service.PackageCreationInfo{
   335  			PackageInfo: packages_service.PackageInfo{
   336  				Owner:       ctx.Package.Owner,
   337  				PackageType: packages_model.TypeSwift,
   338  				Name:        buildPackageID(packageScope, packageName),
   339  				Version:     packageVersion,
   340  			},
   341  			SemverCompatible: true,
   342  			Creator:          ctx.Doer,
   343  			Metadata:         pck.Metadata,
   344  			PackageProperties: map[string]string{
   345  				swift_module.PropertyScope: packageScope,
   346  				swift_module.PropertyName:  packageName,
   347  			},
   348  		},
   349  		&packages_service.PackageFileCreationInfo{
   350  			PackageFileInfo: packages_service.PackageFileInfo{
   351  				Filename: fmt.Sprintf("%s-%s.zip", packageName, packageVersion),
   352  			},
   353  			Creator: ctx.Doer,
   354  			Data:    buf,
   355  			IsLead:  true,
   356  		},
   357  	)
   358  	if err != nil {
   359  		switch err {
   360  		case packages_model.ErrDuplicatePackageVersion:
   361  			apiError(ctx, http.StatusConflict, err)
   362  		case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
   363  			apiError(ctx, http.StatusForbidden, err)
   364  		default:
   365  			apiError(ctx, http.StatusInternalServerError, err)
   366  		}
   367  		return
   368  	}
   369  
   370  	for _, url := range pck.RepositoryURLs {
   371  		_, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, swift_module.PropertyRepositoryURL, url)
   372  		if err != nil {
   373  			log.Error("InsertProperty failed: %v", err)
   374  		}
   375  	}
   376  
   377  	setResponseHeaders(ctx.Resp, &headers{})
   378  
   379  	ctx.Status(http.StatusCreated)
   380  }
   381  
   382  // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-4
   383  func DownloadPackageFile(ctx *context.Context) {
   384  	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, buildPackageID(ctx.Params("scope"), ctx.Params("name")), ctx.Params("version"))
   385  	if err != nil {
   386  		if errors.Is(err, util.ErrNotExist) {
   387  			apiError(ctx, http.StatusNotFound, err)
   388  		} else {
   389  			apiError(ctx, http.StatusInternalServerError, err)
   390  		}
   391  		return
   392  	}
   393  
   394  	pd, err := packages_model.GetPackageDescriptor(ctx, pv)
   395  	if err != nil {
   396  		apiError(ctx, http.StatusInternalServerError, err)
   397  		return
   398  	}
   399  
   400  	pf := pd.Files[0].File
   401  
   402  	s, u, _, err := packages_service.GetPackageFileStream(ctx, pf)
   403  	if err != nil {
   404  		apiError(ctx, http.StatusInternalServerError, err)
   405  		return
   406  	}
   407  
   408  	setResponseHeaders(ctx.Resp, &headers{
   409  		Digest: pd.Files[0].Blob.HashSHA256,
   410  	})
   411  
   412  	helper.ServePackageFile(ctx, s, u, pf, &context.ServeHeaderOptions{
   413  		Filename:     pf.Name,
   414  		ContentType:  "application/zip",
   415  		LastModified: pf.CreatedUnix.AsLocalTime(),
   416  	})
   417  }
   418  
   419  type LookupPackageIdentifiersResponse struct {
   420  	Identifiers []string `json:"identifiers"`
   421  }
   422  
   423  // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-5
   424  func LookupPackageIdentifiers(ctx *context.Context) {
   425  	url := ctx.FormTrim("url")
   426  	if url == "" {
   427  		apiError(ctx, http.StatusBadRequest, nil)
   428  		return
   429  	}
   430  
   431  	pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
   432  		OwnerID: ctx.Package.Owner.ID,
   433  		Type:    packages_model.TypeSwift,
   434  		Properties: map[string]string{
   435  			swift_module.PropertyRepositoryURL: url,
   436  		},
   437  		IsInternal: optional.Some(false),
   438  	})
   439  	if err != nil {
   440  		apiError(ctx, http.StatusInternalServerError, err)
   441  		return
   442  	}
   443  
   444  	if len(pvs) == 0 {
   445  		apiError(ctx, http.StatusNotFound, nil)
   446  		return
   447  	}
   448  
   449  	pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
   450  	if err != nil {
   451  		apiError(ctx, http.StatusInternalServerError, err)
   452  		return
   453  	}
   454  
   455  	identifiers := make([]string, 0, len(pds))
   456  	for _, pd := range pds {
   457  		identifiers = append(identifiers, pd.Package.Name)
   458  	}
   459  
   460  	setResponseHeaders(ctx.Resp, &headers{})
   461  
   462  	ctx.JSON(http.StatusOK, LookupPackageIdentifiersResponse{
   463  		Identifiers: identifiers,
   464  	})
   465  }