code.gitea.io/gitea@v1.21.7/routers/api/packages/pypi/pypi.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package pypi
     5  
     6  import (
     7  	"encoding/hex"
     8  	"io"
     9  	"net/http"
    10  	"regexp"
    11  	"sort"
    12  	"strings"
    13  
    14  	packages_model "code.gitea.io/gitea/models/packages"
    15  	"code.gitea.io/gitea/modules/context"
    16  	packages_module "code.gitea.io/gitea/modules/packages"
    17  	pypi_module "code.gitea.io/gitea/modules/packages/pypi"
    18  	"code.gitea.io/gitea/modules/setting"
    19  	"code.gitea.io/gitea/modules/validation"
    20  	"code.gitea.io/gitea/routers/api/packages/helper"
    21  	packages_service "code.gitea.io/gitea/services/packages"
    22  )
    23  
    24  // https://peps.python.org/pep-0426/#name
    25  var (
    26  	normalizer  = strings.NewReplacer(".", "-", "_", "-")
    27  	nameMatcher = regexp.MustCompile(`\A(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\.\-_]*[a-zA-Z0-9])\z`)
    28  )
    29  
    30  // https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
    31  var versionMatcher = regexp.MustCompile(`\Av?` +
    32  	`(?:[0-9]+!)?` + // epoch
    33  	`[0-9]+(?:\.[0-9]+)*` + // release segment
    34  	`(?:[-_\.]?(?:a|b|c|rc|alpha|beta|pre|preview)[-_\.]?[0-9]*)?` + // pre-release
    35  	`(?:-[0-9]+|[-_\.]?(?:post|rev|r)[-_\.]?[0-9]*)?` + // post release
    36  	`(?:[-_\.]?dev[-_\.]?[0-9]*)?` + // dev release
    37  	`(?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)?` + // local version
    38  	`\z`)
    39  
    40  func apiError(ctx *context.Context, status int, obj any) {
    41  	helper.LogAndProcessError(ctx, status, obj, func(message string) {
    42  		ctx.PlainText(status, message)
    43  	})
    44  }
    45  
    46  // PackageMetadata returns the metadata for a single package
    47  func PackageMetadata(ctx *context.Context) {
    48  	packageName := normalizer.Replace(ctx.Params("id"))
    49  
    50  	pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypePyPI, packageName)
    51  	if err != nil {
    52  		apiError(ctx, http.StatusInternalServerError, err)
    53  		return
    54  	}
    55  	if len(pvs) == 0 {
    56  		apiError(ctx, http.StatusNotFound, err)
    57  		return
    58  	}
    59  
    60  	pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
    61  	if err != nil {
    62  		apiError(ctx, http.StatusInternalServerError, err)
    63  		return
    64  	}
    65  
    66  	// sort package descriptors by version to mimic PyPI format
    67  	sort.Slice(pds, func(i, j int) bool {
    68  		return strings.Compare(pds[i].Version.Version, pds[j].Version.Version) < 0
    69  	})
    70  
    71  	ctx.Data["RegistryURL"] = setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/pypi"
    72  	ctx.Data["PackageDescriptor"] = pds[0]
    73  	ctx.Data["PackageDescriptors"] = pds
    74  	ctx.HTML(http.StatusOK, "api/packages/pypi/simple")
    75  }
    76  
    77  // DownloadPackageFile serves the content of a package
    78  func DownloadPackageFile(ctx *context.Context) {
    79  	packageName := normalizer.Replace(ctx.Params("id"))
    80  	packageVersion := ctx.Params("version")
    81  	filename := ctx.Params("filename")
    82  
    83  	s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
    84  		ctx,
    85  		&packages_service.PackageInfo{
    86  			Owner:       ctx.Package.Owner,
    87  			PackageType: packages_model.TypePyPI,
    88  			Name:        packageName,
    89  			Version:     packageVersion,
    90  		},
    91  		&packages_service.PackageFileInfo{
    92  			Filename: filename,
    93  		},
    94  	)
    95  	if err != nil {
    96  		if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
    97  			apiError(ctx, http.StatusNotFound, err)
    98  			return
    99  		}
   100  		apiError(ctx, http.StatusInternalServerError, err)
   101  		return
   102  	}
   103  
   104  	helper.ServePackageFile(ctx, s, u, pf)
   105  }
   106  
   107  // UploadPackageFile adds a file to the package. If the package does not exist, it gets created.
   108  func UploadPackageFile(ctx *context.Context) {
   109  	file, fileHeader, err := ctx.Req.FormFile("content")
   110  	if err != nil {
   111  		apiError(ctx, http.StatusBadRequest, err)
   112  		return
   113  	}
   114  	defer file.Close()
   115  
   116  	buf, err := packages_module.CreateHashedBufferFromReader(file)
   117  	if err != nil {
   118  		apiError(ctx, http.StatusInternalServerError, err)
   119  		return
   120  	}
   121  	defer buf.Close()
   122  
   123  	_, _, hashSHA256, _ := buf.Sums()
   124  
   125  	if !strings.EqualFold(ctx.Req.FormValue("sha256_digest"), hex.EncodeToString(hashSHA256)) {
   126  		apiError(ctx, http.StatusBadRequest, "hash mismatch")
   127  		return
   128  	}
   129  
   130  	if _, err := buf.Seek(0, io.SeekStart); err != nil {
   131  		apiError(ctx, http.StatusInternalServerError, err)
   132  		return
   133  	}
   134  
   135  	packageName := normalizer.Replace(ctx.Req.FormValue("name"))
   136  	packageVersion := ctx.Req.FormValue("version")
   137  	if !isValidNameAndVersion(packageName, packageVersion) {
   138  		apiError(ctx, http.StatusBadRequest, "invalid name or version")
   139  		return
   140  	}
   141  
   142  	projectURL := ctx.Req.FormValue("home_page")
   143  	if !validation.IsValidURL(projectURL) {
   144  		projectURL = ""
   145  	}
   146  
   147  	_, _, err = packages_service.CreatePackageOrAddFileToExisting(
   148  		ctx,
   149  		&packages_service.PackageCreationInfo{
   150  			PackageInfo: packages_service.PackageInfo{
   151  				Owner:       ctx.Package.Owner,
   152  				PackageType: packages_model.TypePyPI,
   153  				Name:        packageName,
   154  				Version:     packageVersion,
   155  			},
   156  			SemverCompatible: false,
   157  			Creator:          ctx.Doer,
   158  			Metadata: &pypi_module.Metadata{
   159  				Author:          ctx.Req.FormValue("author"),
   160  				Description:     ctx.Req.FormValue("description"),
   161  				LongDescription: ctx.Req.FormValue("long_description"),
   162  				Summary:         ctx.Req.FormValue("summary"),
   163  				ProjectURL:      projectURL,
   164  				License:         ctx.Req.FormValue("license"),
   165  				RequiresPython:  ctx.Req.FormValue("requires_python"),
   166  			},
   167  		},
   168  		&packages_service.PackageFileCreationInfo{
   169  			PackageFileInfo: packages_service.PackageFileInfo{
   170  				Filename: fileHeader.Filename,
   171  			},
   172  			Creator: ctx.Doer,
   173  			Data:    buf,
   174  			IsLead:  true,
   175  		},
   176  	)
   177  	if err != nil {
   178  		switch err {
   179  		case packages_model.ErrDuplicatePackageFile:
   180  			apiError(ctx, http.StatusBadRequest, err)
   181  		case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
   182  			apiError(ctx, http.StatusForbidden, err)
   183  		default:
   184  			apiError(ctx, http.StatusInternalServerError, err)
   185  		}
   186  		return
   187  	}
   188  
   189  	ctx.Status(http.StatusCreated)
   190  }
   191  
   192  func isValidNameAndVersion(packageName, packageVersion string) bool {
   193  	return nameMatcher.MatchString(packageName) && versionMatcher.MatchString(packageVersion)
   194  }