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